From 76b2269456a7e1df250ef39be7c3594371243649 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 14:22:17 +0000 Subject: [PATCH 01/60] docs(plans): browser surfaces PWA-parity review Gap analysis of the embedded web tab and full-screen browser (top pull-down and bottom console sheets, WebView behaviours) against an installed Chrome PWA, with bugs found, a unified chrome proposal, and a phased plan. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TEkj7Eo2xbidVHAoF8GZKQ --- .../plans/2026-09-26-browser-pwa-parity.md | 269 ++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 amethyst/plans/2026-09-26-browser-pwa-parity.md diff --git a/amethyst/plans/2026-09-26-browser-pwa-parity.md b/amethyst/plans/2026-09-26-browser-pwa-parity.md new file mode 100644 index 0000000000..e9c658cf62 --- /dev/null +++ b/amethyst/plans/2026-09-26-browser-pwa-parity.md @@ -0,0 +1,269 @@ +# Browser surfaces → PWA parity review + +Status: **review / proposal** (no code changed yet). Scope: every surface that renders a +plain web client — the **embedded** bottom-bar tab and the **external** full-screen browser — +plus their shared top pull-down "pill" and bottom console "pill". The nsite/napplet hosts +reuse the same chrome and are covered where the change is shared. + +Benchmark: an installed PWA in Chrome for Android (WebAPK, `display: standalone` / +`minimal-ui`), including the Custom-Tab-style bar Chrome shows when a PWA navigates out of +its scope. + +## 1. Surfaces today + +| Surface | Code | Process / rendering | Chrome | +|---|---|---|---| +| **Embedded web tab** (bottom-bar favorite) | `WebAppScreen` → `EmbeddedWebAppController` → `NappletBrowserService` | `:napplet` WebView streamed via SurfaceControlViewHost (API 30+) | Compose `TopControlSheet` + `BottomConsoleSheet`, drawn by `EmbeddedTabLayer` | +| **External browser** (Browser tab launcher, "Open full") | `BrowserScreen` → `FavoriteAppLauncher.launchUrl` → `NappletBrowserActivity` | `:napplet` WebView hosted directly; own task (`documentLaunchMode=intoExisting`) | native-View `NappletControlSheet` + `NappletConsolePanel` | +| Embedded / full nsite + napplet | `NostrAppScreen` / `NappletHostService` / `NappletHostActivity` | same split, verified-blob shell | same two chrome implementations, `isSandbox = true` | + +So there are **two implementations of the same chrome** (Compose + plain Views), and they have +drifted. + +### Top pill — current contents + +| Row | Embedded (Compose) | External (Views) | +|---|---|---| +| Header | icon + host title | emoji 🌐/🛡 + **launch-time** title (never updates) | +| Address | — | **editable** URL field + 🔒/🧅/🌐 glyph | +| Tor | switch, `Lock` icon, `favorite_app_network_*` strings | switch, `ic_tor` icon, `napplet_net_*` strings | +| Reload | ✓ | ✓ (glyph `↻`) | +| Access info | sandbox only | sandbox only (glyph `ⓘ`) | +| Manage permissions | ✓ (navigates in-app) | ✓ (glyph `⚙`, IPC → broker) | +| Open full screen | ✓ | — (no way back to the tab) | +| Favorite | ✓, reflects registry live | ✓, but **resets to "Add" on every navigation** | +| Console | switch | switch (glyph `>`) | + +### Bottom pill + +Console only: hidden until the top-pill Console switch turns it on, then a grabber + log panel +(max 40 % height in Compose, a fixed 220 dp in Views). Neither surface has a bottom +navigation/actions pill. + +## 2. What a Chrome PWA gives the user + +**Window / OS integration** +- Its own launcher icon (Add to Home screen / Install), its own task in Recents with the + **app's name, icon and `theme_color`**, and a splash screen built from the manifest. +- Status bar (and nav bar) tinted with `theme_color` / ``, updated + live when the page changes it. +- `display_override` / `display` modes: `standalone` (no UI), `minimal-ui` (back + reload), + `fullscreen`. +- Launch handling: `start_url`, `scope`, `launch_handler`; deep links into the scope open the + PWA; app shortcuts (manifest `shortcuts`, shown on launcher long-press); `share_target` + (appears in the Android share sheet). + +**Navigation** +- System back = history back; at the root, back leaves the app. +- **Out-of-scope** navigation (e.g. an OAuth provider) opens a Custom-Tab-like bar showing the + **origin + security state + ✕ close**, and returns to the app when closed. +- `target=_blank` / `window.open()` open a new Chrome tab (or a popup window for OAuth) and + `window.opener`/`postMessage` works. +- Non-web schemes (`mailto:`, `tel:`, `intent:`, `geo:`, `nostr:`…) hand off to apps. + +**App menu** (minimal-ui ⋮, or the out-of-scope bar ⋮) — top icon row +`Forward · Reload/Stop`, then: Share…, Copy link, Open in Chrome, Find in page, Desktop site, +Zoom/text size, Page info (connection, certificate, cookies, site settings / permissions), +Clear site data, App info. + +**Web platform APIs that "just work"** +- JS dialogs (`alert`/`confirm`/`prompt`, `beforeunload`), labelled with the origin. +- Permission prompts: camera/mic (`getUserMedia`), geolocation, notifications + Push, + clipboard read, MIDI, protected media — with a per-origin site-settings page. +- Downloads (``, `Content-Disposition`, blob: URLs) to the Downloads folder. +- HTML fullscreen (`requestFullscreen`, the video player's fullscreen button), screen + orientation lock, Wake Lock. +- Web Share (`navigator.share`) and Share Target, Badging API, Contact Picker, File System + Access (open/save pickers), ``. +- Long-press context menu on links/images (open in new tab, copy link, share, download + image), text selection toolbar with Share/Translate/Search. +- Pull-to-refresh (unless the page sets `overscroll-behavior`). +- Service worker offline support, a proper offline/error page with Retry. + +## 3. Gap analysis + +Legend: ✅ parity · ⚠️ partial · ❌ missing · 🔒 intentionally blocked (sandbox/Tor/keyless — keep it or gate it behind consent). + +| Capability | Embedded | External | Notes | +|---|---|---|---| +| Own task/Recents entry | n/a (it's a tab) | ⚠️ | Own task exists, but Recents shows the Amethyst label/icon — no `setTaskDescription(title, favicon, themeColor)`. | +| Add to Home screen / install | ❌ | ❌ | No `ShortcutManagerCompat.requestPinShortcut`. The launcher intent must route through the main process (the `:napplet` activity isn't exported). | +| Theme-color system bars | ❌ | ❌ | External pads the window with the app's `colorBackground`; `theme-color` is never read. | +| Page title in chrome | ⚠️ host only | ❌ fixed at launch | Neither listens to `onReceivedTitle`. | +| System back = history back | ✅ | ✅ | | +| Forward | ❌ | ❌ | Chrome's menu icon row starts with Forward. | +| Stop loading | ❌ | ❌ | Reload stays Reload while loading. | +| Out-of-scope bar (origin + ✕ close) | ❌ | ❌ | Navigating off-site silently replaces the app. There's no concept of the app's "scope" or "home". | +| `target=_blank` / `window.open` | ❌ | ❌ | `setSupportMultipleWindows(false)` + `javaScriptCanOpenWindowsAutomatically=false`: `_blank` loads in place, `window.open()` returns `null`, so OAuth popups break. | +| Non-http schemes | ⚠️ | ⚠️ | Handed to `ACTION_VIEW` on a gesture, but `intent:` URIs aren't parsed (`Intent.parseUri` + `browser_fallback_url`), so they fail. | +| JS dialogs | ❌ | ❌ | **Confirmed from source:** the framework `JsDialogHelper` only shows a dialog when `webView.context is Activity`. The embed runs on a Service context, and the external browser's WebView uses `nightThemedContext()` (a `ContextThemeWrapper` over `createConfigurationContext`, not an Activity) whenever the theme is DARK/LIGHT, which `FavoriteAppLauncher` always resolves it to. So `confirm()` returns `false` and `prompt()` returns `null` in both. | +| Permission prompts (camera/mic) | ❌ | ❌ | `onPermissionRequest` isn't overridden, so the default denies. Video calls, QR scanners and voice notes on the web all fail. | +| Geolocation | 🔒 | 🔒 | `setGeolocationEnabled(false)`. Could be offered per-origin behind consent, off under Tor. | +| Notifications / Push | ❌ | ❌ | Not available in WebView at all. Only a bridge polyfill could provide it; out of scope for v1. | +| Downloads | ❌ | ❌ | No `DownloadListener`, so download links do nothing. | +| HTML fullscreen video | ❌ | ❌ | `onShowCustomView` isn't implemented, so the fullscreen button is dead. | +| Web Share (`navigator.share`) | ❌ | ❌ | Not in WebView. Can be polyfilled through the existing document-start shim to an `ACTION_SEND` chooser. | +| Share page / Copy link | ❌ | ❌ | Not in either top pill. | +| Open in external browser | ❌ | ❌ | Useful escape hatch (Google sign-in blocks WebView user agents). | +| Find in page | ❌ | ❌ | `WebView.findAllAsync` / `findNext`. | +| Desktop site | ❌ | ❌ | UA override + `useWideViewPort`. | +| Text zoom | ❌ | ❌ | `settings.textZoom`. | +| Page info / site settings | ⚠️ | ⚠️ | "Manage permissions" covers NIP-07 grants only. No connection/cert state, no clear-site-data. | +| Long-press link/image menu | ❌ | ❌ | External can use `hitTestResult` in `onCreateContextMenu`. For the embed, the tap forwarder would need a long-press path. | +| Pull-to-refresh | ❌ | ❌ | Optional. Should respect the page's overscroll (only fire at `scrollY == 0`). | +| Error / offline page | ✅ overlay + Retry | ⚠️ | External only logs to the console and shows the raw WebView error page. | +| Renderer crash recovery | ❌ | ❌ | No `onRenderProcessGone`. A renderer crash in `:napplet` kills every WebView in that process, including every warm tab. | +| File input | ✅ | ✅ | Recently added. | +| NIP-07 `window.nostr` | ✅ | ✅ | Amethyst-only advantage. Keep it. | +| Tor per host | ✅ | ✅ | Amethyst-only advantage. | + +### Bugs found along the way (fix regardless of the redesign) + +1. **"Open full screen" opens the launch URL, not the current page.** In `WebAppScreen` it + calls `FavoriteAppLauncher.launchUrl(context, url)` with the original `url`, not + `currentUrl`. +2. **External favorite state is wrong after any navigation.** `NappletControlSheet.updateUrl` + forces `isFavorite = false`, so an already-pinned site shows "Add to favorites" and tapping + it sends a toggle that *removes* the pin. The broker knows the truth: it should push the + state back (or the toggle should be an explicit add/remove, not a blind flip). +3. **External header title never updates.** It shows the launch host even after navigating to + another site. That's misleading when combined with NIP-07 prompts. +4. **The two sheets disagree.** They use different icons (emoji vs Material symbols, `Lock` vs + `ic_tor`), different strings for the same row, a different row order, and only one side has + "Open full" and the address field. There's also a doc contradiction: `BrowserScreen` says "a + running app never carries an editable address bar", but `NappletControlSheet` renders one + in the external browser. + +## 4. Proposal + +### 4.1 One chrome spec, two renderers + +Promote `EmbeddedTabChrome` into a surface-neutral **`WebChromeSpec`** in `commons`: title, +origin, security state (https / http / onion-via-Tor / sandbox), canGoBack/Forward, +isLoading, isFavorite, theme color, and a list of typed actions. Both renderers draw from +it with the same order, icons (Material Symbols font; the Views side can draw the same glyphs +from `material_symbols_outlined.ttf` with a `Typeface`) and strings. Add a unit test that pins +the row order and visibility for each surface type (web / nsite / napplet × embed / full). + +### 4.2 Top pill (Chrome-style app menu) + +Keep the top-center grabber (it stays out of the site's avatar corner and can't be spoofed +by the page). Expanded: + +``` +┌──────────────────────────────────────────────┐ +│ [favicon] Page title [✕] │ ✕ only in external = close task +│ 🔒 example.com · via Tor │ read-only origin chip; tap → Page info +├──────────────────────────────────────────────┤ +│ ← → ↻/✕ ★ ⇪ │ back · forward · reload/stop · favorite · share +├──────────────────────────────────────────────┤ +│ ⧉ Copy link │ +│ ⬈ Open in browser app │ external browser escape hatch +│ ⛶ Open full screen / ⤓ Return to tab │ embed ↔ external +│ ⌂ Add to Home screen │ +│ 🔍 Find in page │ +│ 🖥 Desktop site [ ] │ +│ 🧅 Route over Tor [●] │ +│ ⚙ Site settings & permissions │ NIP-07 grants + camera/mic/location + clear data +│ >_ Console (N) [ ] │ under a "Developer" divider +└──────────────────────────────────────────────┘ +``` + +- **Drop the editable address field from running apps.** A PWA never has one. The origin chip + is read-only (tap = page info, long-press = copy). Typing a URL stays the Browser tab + launcher's job, which already has the omnibox and suggestions. This matches the stated + intent in `BrowserScreen`'s KDoc. +- Header title comes from `onReceivedTitle`, falling back to the host. +- The icon row mirrors Chrome's top row. Reload turns into Stop while `isLoading`. +- A **scope indicator**: when the current origin differs from the app's start origin, tint the + origin chip and show a "Back to " action (the in-app version of Chrome's out-of-scope + bar). + +### 4.3 Bottom pill + +- Keep it **developer-only** (Console), but move the toggle under a *Developer* divider in the + top pill, and let the grabber show an error-count badge so the user knows why to open it. +- Unify height: 40 % of the surface in both renderers (Views currently hard-code 220 dp). +- **Find-in-page** reuses the bottom slot: a bar with the query field, `n/m`, ↑ ↓ and ✕ docked + where the console grabber sits (Chrome puts find-in-page at the top, but the top edge here + belongs to the grabber, and the bottom avoids covering the page's own header). Only one + bottom panel is shown at a time. +- Out-of-scope navigation can also surface a slim bottom "← Back to " pill. Pick either + this or the chip in 4.2 after trying both on a device. + +### 4.4 Behaviours (WebView plumbing) + +Fix these in `NappletBrowserActivity`, `NappletBrowserService`, and where applicable the +nsite/napplet hosts (sandbox profile permitting). + +1. **JS dialogs:** implement `onJsAlert`/`onJsConfirm`/`onJsPrompt`/`onJsBeforeUnload` ourselves. + - External: show an Activity-owned dialog titled "*origin* says". + - Embed: IPC to the main process and show a Compose dialog over the tab. + - Sandboxed napplets keep today's auto-cancel. +2. **Popups / `_blank`:** enable `setSupportMultipleWindows(true)` and handle `onCreateWindow`. + A user-gesture `window.open` becomes a transient child WebView (an OAuth popup, with + `opener` intact) shown as a sheet with origin + ✕. A plain `_blank` link opens a new + external-browser task. Keep auto-open without a gesture blocked. +3. **`intent:` URIs:** parse them with `Intent.parseUri(..., URI_INTENT_SCHEME)`, strip the + component/selector, and fall back to `browser_fallback_url`. Keep the gesture requirement. +4. **Downloads:** add a `DownloadListener`. Hand off to the main process, which runs + `DownloadManager` (through the Tor proxy when the host is on Tor, or else refuses rather + than leaking the download). Handle `blob:`/`data:` via the shim. +5. **Permissions:** handle `onPermissionRequest` (camera/mic) and optionally geolocation. The + consent prompt runs in the main process (same pattern as NIP-07 consent), is stored per + origin in the existing Connected Apps ledger, and requests the Android runtime permission + from the main activity. Geolocation stays off by default and off under Tor. +6. **Fullscreen:** implement `onShowCustomView`/`onHideCustomView`. + - External: swap in the custom view with immersive system bars. + - Embed: open the external browser in fullscreen, since a streamed surface can't take over + the window. +7. **Web Share polyfill:** have the document-start shim define `navigator.share`/`canShare` + routed over the existing bridge to an `ACTION_SEND` chooser (text/url, files later). + Require a user activation. +8. **Theme color:** a tiny shim observer reports `` (and changes to + it) over the bridge. External tints the status/nav bar padding and uses it in + `setTaskDescription`. Embed tints the top grabber. +9. **Task description (external):** call `setTaskDescription(title, favicon, themeColor)` on + title, icon and theme changes, so Recents looks like an installed app. +10. **Add to Home screen:** `ShortcutManagerCompat.requestPinShortcut`, with the favicon (from + `BrowserIconRegistry`) as the icon and an intent to an exported main-process trampoline + that calls `FavoriteAppLauncher.launchUrl`. Offer "open as tab" vs "open full screen". + Also add dynamic shortcuts for the top favorites on launcher long-press. +11. **Renderer crash:** handle `onRenderProcessGone`. Destroy that WebView, return `true`, and + show the error overlay with Retry (for the embed, rebuild the session on Retry). +12. **Context menu (external first):** on long-press over a link or image, offer Open in new + window, Copy link, Share link, Download image. +13. **Desktop site / text zoom:** per-host settings persisted next to `WebAppNetworkRegistry`. +14. **Pull-to-refresh (optional):** only when the page is at `scrollY == 0` and hasn't claimed + overscroll. + +### 4.5 Deliberately different from Chrome (keep) + +- Keyless `:napplet` process, per-origin NIP-07 consent, per-account WebView profile, Tor + routing per host. These are Amethyst's reason to exist and none of the above weakens them. + Every new capability goes through the broker with the same consent + ledger pattern. +- Notifications/Push: not feasible in WebView without a service-worker bridge. Out of scope. +- Service-worker offline: WebView already supports SW. Nothing to do beyond not clearing the + profile. + +## 5. Phasing + +| Phase | Contents | Size | +|---|---|---| +| **0: bugs** | §3 bugs 1–3; `onReceivedTitle`; `onRenderProcessGone` | S | +| **1: chrome unification** | `WebChromeSpec` in commons; both renderers; icon row (back/forward/reload-stop/star/share); Copy link; Open in browser app; Return to tab; remove the external address field; row-order test | M | +| **2: dead web APIs** | JS dialogs, `_blank`/`window.open` popups, `intent:` URIs, downloads, fullscreen video, Web Share polyfill | M–L | +| **3: OS integration** | theme-color bars, `setTaskDescription`, Add to Home screen + trampoline, dynamic shortcuts | M | +| **4: permissions & page info** | camera/mic/geo consent via the broker, site settings page (grants + clear data + connection state), Find in page, Desktop site, text zoom | L | +| **5: polish** | long-press context menu, pull-to-refresh, out-of-scope indicator | M | + +## 6. Open questions + +1. Should running apps lose the editable address bar entirely (the PWA model), or keep it + behind a "Go to address…" row? +2. Where does the user land on a new-window or `_blank` link from an **embedded** tab: a new + external task (Chrome PWA behaviour) or a transient sheet over the tab? +3. Camera/mic/geo: allow over Tor at all? (WebRTC can leak the real IP; geolocation defeats + the point.) The proposal is to deny both when the host is on Tor. +4. Add to Home screen: should the shortcut open as an in-app tab or as the external + standalone window by default? From 684b9ba0a4191d79baaab9bcb36494287627e0e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 14:53:38 +0000 Subject: [PATCH 02/60] docs(plans): record browser PWA-parity decisions Address bar hidden behind an 'Edit address' row, new windows follow Chrome (new full-screen Amethyst browser window, opener kept), camera/mic/location consent-gated on Tor and open web alike, home-screen shortcuts open in Amethyst's own browser via a main-process trampoline. Phase 0 marked done; renderer-crash handling moved to phase 2 since every :napplet WebView owner must handle it together. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TEkj7Eo2xbidVHAoF8GZKQ --- .../plans/2026-09-26-browser-pwa-parity.md | 84 ++++++++++++------- 1 file changed, 52 insertions(+), 32 deletions(-) diff --git a/amethyst/plans/2026-09-26-browser-pwa-parity.md b/amethyst/plans/2026-09-26-browser-pwa-parity.md index e9c658cf62..0321a63f50 100644 --- a/amethyst/plans/2026-09-26-browser-pwa-parity.md +++ b/amethyst/plans/2026-09-26-browser-pwa-parity.md @@ -1,6 +1,6 @@ # Browser surfaces → PWA parity review -Status: **review / proposal** (no code changed yet). Scope: every surface that renders a +Status: **review / proposal**, decisions recorded in §6. Scope: every surface that renders a plain web client — the **embedded** bottom-bar tab and the **external** full-screen browser — plus their shared top pull-down "pill" and bottom console "pill". The nsite/napplet hosts reuse the same chrome and are covered where the change is shared. @@ -98,7 +98,7 @@ Legend: ✅ parity · ⚠️ partial · ❌ missing · 🔒 intentionally blocke | Non-http schemes | ⚠️ | ⚠️ | Handed to `ACTION_VIEW` on a gesture, but `intent:` URIs aren't parsed (`Intent.parseUri` + `browser_fallback_url`), so they fail. | | JS dialogs | ❌ | ❌ | **Confirmed from source:** the framework `JsDialogHelper` only shows a dialog when `webView.context is Activity`. The embed runs on a Service context, and the external browser's WebView uses `nightThemedContext()` (a `ContextThemeWrapper` over `createConfigurationContext`, not an Activity) whenever the theme is DARK/LIGHT, which `FavoriteAppLauncher` always resolves it to. So `confirm()` returns `false` and `prompt()` returns `null` in both. | | Permission prompts (camera/mic) | ❌ | ❌ | `onPermissionRequest` isn't overridden, so the default denies. Video calls, QR scanners and voice notes on the web all fail. | -| Geolocation | 🔒 | 🔒 | `setGeolocationEnabled(false)`. Could be offered per-origin behind consent, off under Tor. | +| Geolocation | ❌ | ❌ | `setGeolocationEnabled(false)`. To be offered per origin behind consent, on Tor and open web alike (§6). | | Notifications / Push | ❌ | ❌ | Not available in WebView at all. Only a bridge polyfill could provide it; out of scope for v1. | | Downloads | ❌ | ❌ | No `DownloadListener`, so download links do nothing. | | HTML fullscreen video | ❌ | ❌ | `onShowCustomView` isn't implemented, so the fullscreen button is dead. | @@ -158,6 +158,7 @@ by the page). Expanded: │ ← → ↻/✕ ★ ⇪ │ back · forward · reload/stop · favorite · share ├──────────────────────────────────────────────┤ │ ⧉ Copy link │ +│ ✎ Edit address │ rare: reveals the editable URL field │ ⬈ Open in browser app │ external browser escape hatch │ ⛶ Open full screen / ⤓ Return to tab │ embed ↔ external │ ⌂ Add to Home screen │ @@ -169,10 +170,11 @@ by the page). Expanded: └──────────────────────────────────────────────┘ ``` -- **Drop the editable address field from running apps.** A PWA never has one. The origin chip - is read-only (tap = page info, long-press = copy). Typing a URL stays the Browser tab - launcher's job, which already has the omnibox and suggestions. This matches the stated - intent in `BrowserScreen`'s KDoc. +- **Hide the editable address field by default** (decision 1). A PWA never shows one. The + origin chip is read-only (tap = page info, long-press = copy). An **Edit address** row, in + both the embed and the external browser, swaps the chip for the editable field with + the URL pre-selected; Go navigates and collapses it back to the chip. Expected to be used + rarely; the Browser tab launcher stays the main place to type a URL. - Header title comes from `onReceivedTitle`, falling back to the host. - The icon row mirrors Chrome's top row. Reload turns into Stop while `isLoading`. - A **scope indicator**: when the current origin differs from the app's start origin, tint the @@ -197,22 +199,32 @@ Fix these in `NappletBrowserActivity`, `NappletBrowserService`, and where applic nsite/napplet hosts (sandbox profile permitting). 1. **JS dialogs:** implement `onJsAlert`/`onJsConfirm`/`onJsPrompt`/`onJsBeforeUnload` ourselves. - - External: show an Activity-owned dialog titled "*origin* says". + - External: show an Activity-owned dialog titled "*origin* says" (the framework helper + can't, because the WebView's themed context isn't an Activity). - Embed: IPC to the main process and show a Compose dialog over the tab. - Sandboxed napplets keep today's auto-cancel. -2. **Popups / `_blank`:** enable `setSupportMultipleWindows(true)` and handle `onCreateWindow`. - A user-gesture `window.open` becomes a transient child WebView (an OAuth popup, with - `opener` intact) shown as a sheet with origin + ✕. A plain `_blank` link opens a new - external-browser task. Keep auto-open without a gesture blocked. +2. **Popups / `_blank` — follow Chrome** (decision 2): enable `setSupportMultipleWindows(true)` + and handle `onCreateWindow`, from both the embed and the external browser. + - Every new window (a `_blank` link or a user-gesture `window.open`) opens as a **new + full-screen Amethyst browser window** (a new `NappletBrowserActivity` task), the way + Chrome opens a new tab. + - `opener` must survive for OAuth popups. The child WebView is created in + `onCreateWindow` (same `:napplet` process as both parents) and handed to the new + activity through an in-process registry keyed by a one-shot token passed in the + intent. `window.close()` from the child (`onCloseWindow`) finishes that task and + returns the user to the opener. + - Keep auto-open without a user gesture blocked (Chrome's popup blocker). 3. **`intent:` URIs:** parse them with `Intent.parseUri(..., URI_INTENT_SCHEME)`, strip the component/selector, and fall back to `browser_fallback_url`. Keep the gesture requirement. 4. **Downloads:** add a `DownloadListener`. Hand off to the main process, which runs `DownloadManager` (through the Tor proxy when the host is on Tor, or else refuses rather than leaking the download). Handle `blob:`/`data:` via the shim. -5. **Permissions:** handle `onPermissionRequest` (camera/mic) and optionally geolocation. The - consent prompt runs in the main process (same pattern as NIP-07 consent), is stored per - origin in the existing Connected Apps ledger, and requests the Android runtime permission - from the main activity. Geolocation stays off by default and off under Tor. +5. **Permissions:** handle `onPermissionRequest` (camera/mic) and + `onGeolocationPermissionsShowPrompt` (enable geolocation). The consent prompt runs in the + main process (same pattern as NIP-07 consent), is stored per origin in the existing + Connected Apps ledger, and requests the Android runtime permission from the main + activity. Works the same on **Tor and the open web** (decision 3); nothing is + auto-denied because of routing. 6. **Fullscreen:** implement `onShowCustomView`/`onHideCustomView`. - External: swap in the custom view with immersive system bars. - Embed: open the external browser in fullscreen, since a streamed surface can't take over @@ -225,12 +237,19 @@ nsite/napplet hosts (sandbox profile permitting). `setTaskDescription`. Embed tints the top grabber. 9. **Task description (external):** call `setTaskDescription(title, favicon, themeColor)` on title, icon and theme changes, so Recents looks like an installed app. -10. **Add to Home screen:** `ShortcutManagerCompat.requestPinShortcut`, with the favicon (from - `BrowserIconRegistry`) as the icon and an intent to an exported main-process trampoline - that calls `FavoriteAppLauncher.launchUrl`. Offer "open as tab" vs "open full screen". - Also add dynamic shortcuts for the top favorites on launcher long-press. +10. **Add to Home screen** (decision 4): `ShortcutManagerCompat.requestPinShortcut`, with the + favicon (from `BrowserIconRegistry`) as the icon. The shortcut always opens the page + **full screen in Amethyst's own browser** (`NappletBrowserActivity`), never the system + browser, so the NIP-07 signer is there. Its intent targets an exported **main-process** + trampoline activity (the `:napplet` activities stay unexported). The trampoline + brings up the broker, the Tor port and the account's WebView profile, then calls + `FavoriteAppLauncher.launchUrl` and finishes. Also add dynamic shortcuts for the top + favorites on launcher long-press, with the same target. 11. **Renderer crash:** handle `onRenderProcessGone`. Destroy that WebView, return `true`, and - show the error overlay with Retry (for the embed, rebuild the session on Retry). + show the error overlay with Retry (for the embed, rebuild the session on Retry). All + WebViews in `:napplet` share one renderer, and the process is still killed if **any** of + them leaves the callback unhandled. So `NappletBrowserActivity`, `NappletBrowserService`, + `NappletHostActivity` and `NappletHostService` must all ship the handler in one change. 12. **Context menu (external first):** on long-press over a link or image, offer Open in new window, Copy link, Share link, Download image. 13. **Desktop site / text zoom:** per-host settings persisted next to `WebAppNetworkRegistry`. @@ -250,20 +269,21 @@ nsite/napplet hosts (sandbox profile permitting). | Phase | Contents | Size | |---|---|---| -| **0: bugs** | §3 bugs 1–3; `onReceivedTitle`; `onRenderProcessGone` | S | -| **1: chrome unification** | `WebChromeSpec` in commons; both renderers; icon row (back/forward/reload-stop/star/share); Copy link; Open in browser app; Return to tab; remove the external address field; row-order test | M | -| **2: dead web APIs** | JS dialogs, `_blank`/`window.open` popups, `intent:` URIs, downloads, fullscreen video, Web Share polyfill | M–L | +| **0: bugs** ✅ | §3 bugs 1–3; live page titles (`onReceivedTitle`) in both surfaces; JS dialogs in the external browser (origin-labelled, with "Block dialogs from this page") | S | +| **1: chrome unification** | `WebChromeSpec` in commons; both renderers; icon row (back/forward/reload-stop/star/share); Copy link; Open in browser app; Return to tab; address field hidden behind "Edit address"; row-order test | M | +| **2: dead web APIs** | `onRenderProcessGone` (all four WebView owners at once — see 4.4 #11), JS dialogs in the embed, `_blank`/`window.open` → new browser window, `intent:` URIs, downloads, fullscreen video, Web Share polyfill | M–L | | **3: OS integration** | theme-color bars, `setTaskDescription`, Add to Home screen + trampoline, dynamic shortcuts | M | | **4: permissions & page info** | camera/mic/geo consent via the broker, site settings page (grants + clear data + connection state), Find in page, Desktop site, text zoom | L | | **5: polish** | long-press context menu, pull-to-refresh, out-of-scope indicator | M | -## 6. Open questions +## 6. Decisions (2026-09-26) -1. Should running apps lose the editable address bar entirely (the PWA model), or keep it - behind a "Go to address…" row? -2. Where does the user land on a new-window or `_blank` link from an **embedded** tab: a new - external task (Chrome PWA behaviour) or a transient sheet over the tab? -3. Camera/mic/geo: allow over Tor at all? (WebRTC can leak the real IP; geolocation defeats - the point.) The proposal is to deny both when the host is on Tor. -4. Add to Home screen: should the shortcut open as an in-app tab or as the external - standalone window by default? +1. **Address bar:** hidden by default in running apps; an "Edit address" row reveals the + editable field. Rarely used. +2. **New windows (`_blank`, `window.open`):** follow Chrome. They open a new full-screen + Amethyst browser window, keeping `opener` for OAuth popups. +3. **Camera / mic / location:** consent-gated per origin, and they work on **both Tor and + the open web**. Routing never auto-denies them. +4. **Add to Home screen:** the shortcut opens the page full screen in **Amethyst's browser** + (not the system browser), so the signer is present. It launches through a main-process + trampoline. From 202b780f7b6ada824d47998b566d6556dfbfe2b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 14:53:41 +0000 Subject: [PATCH 03/60] =?UTF-8?q?fix(browser):=20phase=200=20of=20PWA=20pa?= =?UTF-8?q?rity=20=E2=80=94=20dialogs,=20titles,=20favorite=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Full-screen browser: show page alert/confirm/prompt/beforeunload dialogs ourselves. The framework only shows them when the WebView's context is an Activity, and ours is a configuration context, so they were silently cancelled (confirm() always false). Dialogs are titled with the page's host and offer 'Block dialogs from this page' from the second one on, reset on the next main-frame load. - Full-screen browser: the star no longer resets to 'Add' on every navigation (which made tapping it remove an existing pin). It asks the broker for the real state (MSG_QUERY_WEB_FAVORITE / MSG_WEB_FAVORITE_STATE) and the toggle now sends the explicit target state. - Both surfaces: the top sheet shows the page's , falling back to the current host, instead of the launch-time host. - Embedded tab: 'Open full screen' opens the page being viewed, not the URL the tab was pinned with. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TEkj7Eo2xbidVHAoF8GZKQ --- .../amethyst/napplet/NappletBrokerService.kt | 44 ++++- .../browser/EmbeddedWebAppController.kt | 7 +- .../screen/loggedIn/browser/WebAppScreen.kt | 16 +- .../amethyst/napplethost/BrowserJsDialogs.kt | 181 ++++++++++++++++++ .../napplethost/NappletBrowserActivity.kt | 101 ++++++++-- .../napplethost/NappletBrowserContract.kt | 6 +- .../napplethost/NappletBrowserService.kt | 13 +- .../napplethost/NappletControlSheet.kt | 75 ++++++-- .../amethyst/napplethost/NappletIpc.kt | 25 ++- nappletHost/src/main/res/values/strings.xml | 8 + 10 files changed, 429 insertions(+), 47 deletions(-) create mode 100644 nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserJsDialogs.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt index de5815af52..ed067f5c1a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt @@ -218,18 +218,34 @@ class NappletBrokerService : Service() { return true } - // The direct-WebView browser requests a favorite toggle for the current URL (main process only). + // The direct-WebView browser pins/unpins the page it shows (main process only). if (msg.what == NappletIpc.MSG_TOGGLE_WEB_FAVORITE) { val data = msg.data ?: return true val url = data.getString(NappletIpc.KEY_FAVORITE_URL)?.takeIf { it.isNotBlank() } ?: return true val label = data.getString(NappletIpc.KEY_FAVORITE_LABEL).orEmpty().ifBlank { url } FavoriteAppsRegistry.init(applicationContext) val id = "url:$url" - if (FavoriteAppsRegistry.isFavorite(id)) { - FavoriteAppsRegistry.remove(id) - } else { + val target = + if (data.containsKey(NappletIpc.KEY_FAVORITE_IS_FAVORITE)) { + data.getBoolean(NappletIpc.KEY_FAVORITE_IS_FAVORITE) + } else { + !FavoriteAppsRegistry.isFavorite(id) + } + if (target) { FavoriteAppsRegistry.add(FavoriteApp.WebApp(url, label, System.currentTimeMillis())) + } else { + FavoriteAppsRegistry.remove(id) } + msg.replyTo?.let { replyWebFavoriteState(it, url) } + return true + } + + // The direct-WebView browser asks whether the page it now shows is pinned, so its star is right. + if (msg.what == NappletIpc.MSG_QUERY_WEB_FAVORITE) { + val replyTo = msg.replyTo ?: return true + val url = msg.data?.getString(NappletIpc.KEY_FAVORITE_URL)?.takeIf { it.isNotBlank() } ?: return true + FavoriteAppsRegistry.init(applicationContext) + replyWebFavoriteState(replyTo, url) return true } @@ -460,6 +476,26 @@ class NappletBrokerService : Service() { } } + /** Tells a browser surface whether [url] is currently pinned, so its star shows the registry's truth. */ + private fun replyWebFavoriteState( + replyTo: Messenger, + url: String, + ) { + val message = + Message.obtain(null, NappletIpc.MSG_WEB_FAVORITE_STATE).apply { + data = + Bundle().apply { + putString(NappletIpc.KEY_FAVORITE_URL, url) + putBoolean(NappletIpc.KEY_FAVORITE_IS_FAVORITE, FavoriteAppsRegistry.isFavorite("url:$url")) + } + } + try { + replyTo.send(message) + } catch (e: RemoteException) { + Log.w("NappletBrokerService", "Browser went away before the favorite state could be delivered", e) + } + } + /** Sends an unsolicited push (a `relay.event`/`relay.eose` envelope) for the host to forward verbatim. */ private fun push( replyTo: Messenger, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt index 3858281234..3af22b5bbc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt @@ -106,8 +106,8 @@ class EmbeddedWebAppController( // previous view can never reap the replacement. private var sessionId: String = "browser-${SESSION_SEQ.incrementAndGet()}" - /** Invoked on the main thread when the page navigates: (url, canGoBack). */ - var onUrlChanged: ((String, Boolean) -> Unit)? = null + /** Invoked on the main thread when the page navigates or retitles: (url, title or null, canGoBack). */ + var onUrlChanged: ((String, String?, Boolean) -> Unit)? = null override var onImeEvent: ((ImeEvent) -> Unit)? = null @@ -235,7 +235,8 @@ class EmbeddedWebAppController( NappletBrowserContract.MSG_URL_CHANGED -> { val url = msg.data?.getString(NappletBrowserContract.KEY_URL).orEmpty() val canGoBack = msg.data?.getBoolean(NappletBrowserContract.KEY_CAN_GO_BACK, false) ?: false - onUrlChanged?.invoke(url, canGoBack) + val title = msg.data?.getString(NappletBrowserContract.KEY_TITLE) + onUrlChanged?.invoke(url, title, canGoBack) } NappletBrowserContract.MSG_IME_EVENT -> { val payload = msg.data?.getString(NappletBrowserContract.KEY_IME_PAYLOAD) ?: return true diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/WebAppScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/WebAppScreen.kt index 8d0c221e5a..4c67b7334c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/WebAppScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/WebAppScreen.kt @@ -103,6 +103,8 @@ private fun EmbeddedWebAppTab( val id = "url:$url" var currentUrl by remember { mutableStateOf(url) } + // The page's own <title>; null until the current document reports one (the sheet shows the host). + var pageTitle by remember { mutableStateOf<String?>(null) } var canGoBack by remember { mutableStateOf(false) } val proxyAvailable = remember { Amethyst.instance.torManager.activePortOrNull.value != null } @@ -124,20 +126,24 @@ private fun EmbeddedWebAppTab( // Keep the URL/back callback fresh (cheap, needs the latest closure). SideEffect { - controller.onUrlChanged = { newUrl, back -> - if (newUrl != "about:blank") currentUrl = newUrl + controller.onUrlChanged = { newUrl, title, back -> + if (newUrl != "about:blank") { + currentUrl = newUrl + pageTitle = title + } canGoBack = back } } // Rebuilt only when a displayed value changes, so the tab layer isn't recomposed every frame. val chrome = - remember(currentUrl, torOn, proxyAvailable, isFavorite, controller) { + remember(currentUrl, pageTitle, torOn, proxyAvailable, isFavorite, controller) { EmbeddedTabChrome( - title = hostLabel(currentUrl), + title = pageTitle ?: hostLabel(currentUrl), isSandbox = false, onReload = { controller.reload() }, - onOpenFull = { FavoriteAppLauncher.launchUrl(context, url) }, + // The page the user is looking at, not the one the tab was pinned with. + onOpenFull = { FavoriteAppLauncher.launchUrl(context, currentUrl) }, torOn = if (proxyAvailable) torOn else null, onToggleTor = { torOn = !torOn diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserJsDialogs.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserJsDialogs.kt new file mode 100644 index 0000000000..69ded7e3e3 --- /dev/null +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserJsDialogs.kt @@ -0,0 +1,181 @@ +/* + * 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.napplethost + +import android.app.Activity +import android.app.AlertDialog +import android.text.InputType +import android.webkit.JsPromptResult +import android.webkit.JsResult +import android.widget.EditText +import android.widget.FrameLayout +import androidx.core.net.toUri + +/** + * Shows a web page's `alert()` / `confirm()` / `prompt()` / `beforeunload` dialogs for the full-screen + * browser, the way Chrome does: titled with the page's origin ("example.com says") so a page can't pass + * its dialog off as Amethyst's own, and — from the second dialog on the same page — offering "Block + * dialogs from this page", so a script looping on alert() can't trap the user. The block lasts until the + * next main-frame navigation ([onMainFrameNavigation]). + * + * Every [JsResult] handed in is answered exactly once on every path (button, back, dismissal, a dialog + * already showing, the Activity going away): until it is, the page's JavaScript stays blocked. + */ +internal class BrowserJsDialogs( + private val activity: Activity, +) { + private var showing: AlertDialog? = null + private var shownOnThisPage = 0 + private var blocked = false + + /** A new main-frame load: the page that earned the block is gone. */ + fun onMainFrameNavigation() { + shownOnThisPage = 0 + blocked = false + } + + /** Closes a dialog still up (the Activity is being destroyed); its result is cancelled. */ + fun dismiss() { + showing?.dismiss() + showing = null + } + + fun alert( + url: String?, + message: String?, + result: JsResult, + ): Boolean = + show(result, onBlocked = { result.cancel() }) { answer -> + setTitle(titleFor(url)) + setMessage(message.orEmpty()) + setPositiveButton(android.R.string.ok) { _, _ -> answer { result.confirm() } } + } + + fun confirm( + url: String?, + message: String?, + result: JsResult, + ): Boolean = + show(result, onBlocked = { result.cancel() }) { answer -> + setTitle(titleFor(url)) + setMessage(message.orEmpty()) + setPositiveButton(android.R.string.ok) { _, _ -> answer { result.confirm() } } + setNegativeButton(android.R.string.cancel) { _, _ -> answer { result.cancel() } } + } + + fun prompt( + url: String?, + message: String?, + defaultValue: String?, + result: JsPromptResult, + ): Boolean { + val field = + EditText(activity).apply { + setText(defaultValue.orEmpty()) + setSelectAllOnFocus(true) + isSingleLine = true + inputType = InputType.TYPE_CLASS_TEXT + } + val container = + FrameLayout(activity).apply { + val pad = (20 * activity.resources.displayMetrics.density).toInt() + setPadding(pad, 0, pad, 0) + addView(field) + } + return show(result, onBlocked = { result.cancel() }) { answer -> + setTitle(titleFor(url)) + setMessage(message.orEmpty()) + setView(container) + setPositiveButton(android.R.string.ok) { _, _ -> answer { result.confirm(field.text?.toString().orEmpty()) } } + setNegativeButton(android.R.string.cancel) { _, _ -> answer { result.cancel() } } + } + } + + /** + * "Leave site?" — confirm lets the navigation proceed, cancel keeps the user on the page. The page's own + * message is ignored, as in every current browser (it was abused for scare text). A blocked page may + * no longer hold the user hostage, so blocked means "leave". + */ + fun beforeUnload(result: JsResult): Boolean = + show(result, onBlocked = { result.confirm() }) { answer -> + setTitle(R.string.napplet_js_leave_title) + setMessage(R.string.napplet_js_leave_message) + setPositiveButton(R.string.napplet_js_leave) { _, _ -> answer { result.confirm() } } + setNegativeButton(android.R.string.cancel) { _, _ -> answer { result.cancel() } } + } + + /** + * Builds and shows one dialog. [build] wires the buttons through `answer`, which records that the + * result was settled so the dismiss listener doesn't cancel it a second time. Always returns true: the + * result is ours from here on. + */ + private fun show( + result: JsResult, + onBlocked: () -> Unit, + build: AlertDialog.Builder.(answer: (() -> Unit) -> Unit) -> Unit, + ): Boolean { + if (blocked) { + onBlocked() + return true + } + // One at a time, and never onto a window that is going away. + if (showing != null || activity.isFinishing || activity.isDestroyed) { + result.cancel() + return true + } + var settled = false + val answer: (() -> Unit) -> Unit = { action -> + if (!settled) { + settled = true + action() + } + } + shownOnThisPage++ + val builder = AlertDialog.Builder(activity).apply { build(answer) } + if (shownOnThisPage > 1) { + builder.setNeutralButton(R.string.napplet_js_dialog_block) { _, _ -> + blocked = true + answer { onBlocked() } + } + } + val dialog = + builder + .setOnDismissListener { + showing = null + // Back, a tap outside, or teardown: no button answered, so the page gets a cancel. + answer { result.cancel() } + }.create() + showing = dialog + dialog.show() + return true + } + + private fun titleFor(url: String?): String { + val uri = url?.let { runCatching { it.toUri() }.getOrNull() } + val scheme = uri?.scheme?.lowercase() + val host = uri?.host?.takeIf { it.isNotBlank() } + return if (host != null && (scheme == "http" || scheme == "https")) { + activity.getString(R.string.napplet_js_dialog_title, host) + } else { + activity.getString(R.string.napplet_js_dialog_title_generic) + } + } +} diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt index 8780d82a73..473ccb0e6f 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt @@ -37,6 +37,8 @@ import android.view.Gravity import android.view.View import android.view.ViewGroup import android.webkit.ConsoleMessage +import android.webkit.JsPromptResult +import android.webkit.JsResult import android.webkit.ValueCallback import android.webkit.WebChromeClient import android.webkit.WebResourceError @@ -110,6 +112,13 @@ class NappletBrowserActivity : ComponentActivity() { private var mainFrameLoadFailed = false private var lastIconHost: String? = null + // The last URL whose pin state was asked of the broker, so the several page callbacks that report the + // same address don't each trigger a round-trip. + private var lastFavoriteQueryUrl: String? = null + + // Page-originated alert/confirm/prompt/beforeunload, labelled with the page's origin. + private val jsDialogs by lazy { BrowserJsDialogs(this) } + // ---- HTML file input (`<input type="file">`) ---- // Registered as a field so it is in place before onCreate returns, which is what // registerForActivityResult requires. This activity hosts its WebView directly, so it can run the @@ -306,6 +315,8 @@ class NappletBrowserActivity : ComponentActivity() { // A picker still up when the browser is torn down would otherwise leave its callback unanswered. pendingFileChooser.cancel() fileChooserLauncher.teardown() + // A dialog still up would leak its window and leave the page's JS blocked on an unanswered result. + jsDialogs.dismiss() if (this::webView.isInitialized) { // Detach from the view tree BEFORE destroy(). Destroying a WebView while it is still attached to // the window corrupts the SHARED multiprocess renderer/network state, which then breaks the OTHER @@ -399,6 +410,46 @@ class NappletBrowserActivity : ComponentActivity() { updateLoadProgress(newProgress) } + override fun onReceivedTitle( + view: WebView, + title: String?, + ) { + controlSheet?.updateTitle(title) + } + + // The framework's own JS dialogs only appear when the WebView's context IS an Activity + // (`JsDialogHelper.canShowAlertDialog`), and this one is built from [nightThemedContext] — a + // configuration context, not the Activity — so without these overrides every alert() was silently + // dismissed, confirm() always answered false and prompt() null. [jsDialogs] shows them itself. + override fun onJsAlert( + view: WebView, + url: String?, + message: String?, + result: JsResult, + ): Boolean = jsDialogs.alert(url, message, result) + + override fun onJsConfirm( + view: WebView, + url: String?, + message: String?, + result: JsResult, + ): Boolean = jsDialogs.confirm(url, message, result) + + override fun onJsPrompt( + view: WebView, + url: String?, + message: String?, + defaultValue: String?, + result: JsPromptResult, + ): Boolean = jsDialogs.prompt(url, message, defaultValue, result) + + override fun onJsBeforeUnload( + view: WebView, + url: String?, + message: String?, + result: JsResult, + ): Boolean = jsDialogs.beforeUnload(result) + override fun onReceivedIcon( view: WebView, icon: Bitmap?, @@ -468,7 +519,9 @@ class NappletBrowserActivity : ComponentActivity() { mainFrameLoadFailed = false // Re-arm favicon capture when the host changes, so a same-host in-page nav doesn't re-send. if (OmniboxInput.hostOf(url) != lastIconHost) lastIconHost = null - controlSheet?.updateUrl(url) + // Chrome scopes "block this page's dialogs" to the page: a new main-frame load lifts it. + jsDialogs.onMainFrameNavigation() + showUrl(url) } override fun onReceivedError( @@ -497,7 +550,7 @@ class NappletBrowserActivity : ComponentActivity() { // The page has painted its first frame — drop the loading screen. loadingView?.let { contentFrame.removeView(it) } loadingView = null - controlSheet?.updateUrl(url) + showUrl(url) } override fun doUpdateVisitedHistory( @@ -506,7 +559,7 @@ class NappletBrowserActivity : ComponentActivity() { isReload: Boolean, ) { syncBackState() - controlSheet?.updateUrl(url) + showUrl(url) } override fun onPageFinished( @@ -514,7 +567,7 @@ class NappletBrowserActivity : ComponentActivity() { url: String, ) { syncBackState() - controlSheet?.updateUrl(url) + showUrl(url) // Record only a clean http(s) main-frame load — never a typed-but-failed address. if (!mainFrameLoadFailed && (url.startsWith("https://") || url.startsWith("http://"))) { recordHistory(url, view.title) @@ -523,6 +576,22 @@ class NappletBrowserActivity : ComponentActivity() { } } + /** + * Pushes the displayed [url] into the chrome and, when it is a different page, asks the broker whether + * it is pinned — the registry lives in the main process, so the star can't know on its own. + */ + private fun showUrl(url: String) { + controlSheet?.updateUrl(url) + if (url == lastFavoriteQueryUrl) return + lastFavoriteQueryUrl = url + val msg = + Message.obtain(null, NappletIpc.MSG_QUERY_WEB_FAVORITE).apply { + replyTo = replyMessenger + data = Bundle().apply { putString(NappletIpc.KEY_FAVORITE_URL, url) } + } + if (brokerMessenger != null) sendToBroker(msg) else pendingBrokerRequests.add(msg) + } + /** * Second-chance favicon capture for pages `onReceivedIcon` never fires for (SVG-only declarations — * WebView does not rasterize those into the callback). Deliberately delayed so the WebView's own @@ -677,6 +746,10 @@ class NappletBrowserActivity : ComponentActivity() { val payload = data.getString(NappletIpc.KEY_PAYLOAD) ?: return true bridgeReplyProxy?.postMessage(payload) } + NappletIpc.MSG_WEB_FAVORITE_STATE -> { + val url = data.getString(NappletIpc.KEY_FAVORITE_URL) ?: return true + controlSheet?.setFavorite(url, data.getBoolean(NappletIpc.KEY_FAVORITE_IS_FAVORITE, false)) + } NappletIpc.MSG_BROWSER_TOKEN -> { val origin = data.getString(NappletIpc.KEY_BROWSER_ORIGIN) ?: return true val token = data.getString(NappletIpc.KEY_LAUNCH_TOKEN) ?: return true @@ -754,7 +827,7 @@ class NappletBrowserActivity : ComponentActivity() { onNavigate = { loadAddress(it) }, onConsole = { show -> consolePanel?.setShowing(show) }, isFavoriteInitially = intent.getBooleanExtra(EXTRA_IS_FAVORITE, false), - onFavoriteToggle = { url, _ -> sendFavoriteToggle(url) }, + onFavoriteToggle = { url, isFavorite -> sendFavoriteToggle(url, isFavorite) }, ).also { controlSheet = it } /** @@ -775,19 +848,23 @@ class NappletBrowserActivity : ComponentActivity() { if (brokerMessenger != null) sendToBroker(msg) else pendingBrokerRequests.add(msg) } - private fun sendFavoriteToggle(url: String) { - val host = - runCatching { - android.net.Uri - .parse(url) - .host - }.getOrNull()?.takeIf { it.isNotBlank() } ?: url + /** + * Pins or unpins [url] in the main-process registry. Sends the state the user asked for rather than a + * flip, and takes the broker's confirmed state back through [NappletIpc.MSG_WEB_FAVORITE_STATE]. + */ + private fun sendFavoriteToggle( + url: String, + isFavorite: Boolean, + ) { + val host = runCatching { url.toUri().host }.getOrNull()?.takeIf { it.isNotBlank() } ?: url val msg = Message.obtain(null, NappletIpc.MSG_TOGGLE_WEB_FAVORITE).apply { + replyTo = replyMessenger data = Bundle().apply { putString(NappletIpc.KEY_FAVORITE_URL, url) putString(NappletIpc.KEY_FAVORITE_LABEL, host) + putBoolean(NappletIpc.KEY_FAVORITE_IS_FAVORITE, isFavorite) } } if (brokerMessenger != null) sendToBroker(msg) else pendingBrokerRequests.add(msg) diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserContract.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserContract.kt index 4725eb7074..c4d2ba760a 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserContract.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserContract.kt @@ -50,7 +50,10 @@ object NappletBrowserContract { /** Client → provider: route this session over Tor ([KEY_USE_TOR]) or the open web. */ const val MSG_SET_TOR = 6 - /** Provider → client: the page navigated; carries [KEY_URL] and [KEY_CAN_GO_BACK]. */ + /** + * Provider → client: the page navigated or retitled; carries [KEY_URL], [KEY_CAN_GO_BACK] and, once the + * document has one, its `<title>` in [KEY_TITLE] (absent while a new page is still loading). + */ const val MSG_URL_CHANGED = 7 /** @@ -152,6 +155,7 @@ object NappletBrowserContract { const val KEY_USE_TOR = "useTor" const val KEY_CORE_LIB_INFO = "coreLibInfo" const val KEY_CAN_GO_BACK = "canGoBack" + const val KEY_TITLE = "title" /** * ARGB of Amethyst's theme background, passed from the main process. The WebView (and the surface diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserService.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserService.kt index 474de861fd..665414d757 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserService.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserService.kt @@ -386,6 +386,12 @@ class NappletBrowserService : Service() { recordIcon(host, icon) } + /** Relays the page's `<title>` so the tab's top sheet names the page, not just its host. */ + override fun onReceivedTitle( + view: WebView, + title: String?, + ) = pushUrl(tab, view) + override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { if (tab == null) return false pushConsoleLog( @@ -477,7 +483,8 @@ class NappletBrowserService : Service() { tab?.loadFailed = false // Re-arm favicon capture when the host changes, so a same-host in-page nav doesn't re-send. if (tab != null && OmniboxInput.hostOf(url) != tab.lastIconHost) tab.lastIconHost = null - pushUrl(tab, view) + // view.title still names the page being left; the new one's arrives via onReceivedTitle. + pushUrl(tab, view, includeTitle = false) pushLoadState(tab, view, isLoading = true) } @@ -530,14 +537,18 @@ class NappletBrowserService : Service() { private fun pushUrl( tab: BrowserTab?, view: WebView, + includeTitle: Boolean = true, ) { val url = view.url ?: return + // WebView reports the URL itself as the title of a document that has none yet; that is no title. + val title = view.title?.trim()?.takeIf { includeTitle && it.isNotEmpty() && it != url } val message = Message.obtain(null, NappletBrowserContract.MSG_URL_CHANGED).apply { data = Bundle().apply { putString(NappletBrowserContract.KEY_URL, url) putBoolean(NappletBrowserContract.KEY_CAN_GO_BACK, view.canGoBack()) + title?.let { putString(NappletBrowserContract.KEY_TITLE, it) } } } runCatching { tab?.clientMessenger?.send(message) } diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletControlSheet.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletControlSheet.kt index 28539b2af2..cdc8215eb2 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletControlSheet.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletControlSheet.kt @@ -26,6 +26,7 @@ import android.graphics.Color import android.graphics.Typeface import android.graphics.drawable.GradientDrawable import android.text.InputType +import android.text.TextUtils import android.util.TypedValue import android.view.Gravity import android.view.MotionEvent @@ -37,6 +38,7 @@ import android.widget.LinearLayout import android.widget.Switch import android.widget.TextView import androidx.core.content.ContextCompat +import androidx.core.net.toUri import com.vitorpamplona.amethyst.commons.R as CommonsR /** @@ -52,7 +54,7 @@ import com.vitorpamplona.amethyst.commons.R as CommonsR @SuppressLint("UseSwitchCompatOrMaterialCode") // plain framework Switch: :nappletHost is Compose/Material-free class NappletControlSheet( context: Context, - private val title: String, + title: String, private val isSandbox: Boolean, private val onReload: () -> Unit, torInitiallyOn: Boolean?, @@ -80,12 +82,15 @@ class NappletControlSheet( private val surface = resolveThemeColor(android.R.attr.colorBackground) private var expanded = false + private var title = title private var torOn = torInitiallyOn private var currentUrl = liveUrl private var isFavorite = isFavoriteInitially private var consoleShowing = false private val panel: LinearLayout + private var titleView: TextView? = null + private var grabber: View? = null private var torLabel: TextView? = null private var torSwitch: Switch? = null private var addressField: EditText? = null @@ -99,7 +104,7 @@ class NappletControlSheet( gravity = Gravity.CENTER_HORIZONTAL panel = buildPanel().also { addView(it) } - addView(buildGrabber()) + addView(buildGrabber().also { grabber = it }) } private fun buildPanel(): LinearLayout = @@ -226,13 +231,15 @@ class NappletControlSheet( }, ) addView( - TextView(context).apply { - text = title - setTextColor(onSurface) - textSize = 16f - maxLines = 1 - setPadding(dp(10), 0, 0, 0) - }, + TextView(context) + .apply { + text = title + setTextColor(onSurface) + textSize = 16f + maxLines = 1 + ellipsize = TextUtils.TruncateAt.END + setPadding(dp(10), 0, 0, 0) + }.also { titleView = it }, ) } @@ -304,23 +311,57 @@ class NappletControlSheet( } } - /** Refreshes the address bar + security glyph as the page navigates. No-op without an address row. */ + /** + * Refreshes the address bar + security glyph as the page navigates. No-op without an address row. + * Moving to another site also swaps the title for that site's host until its page title arrives + * ([updateTitle]), so the sheet never names the site the user already left. + */ fun updateUrl(url: String) { + val previous = currentUrl currentUrl = url // Don't fight the user while they're editing the field. addressField?.takeIf { !it.hasFocus() }?.setText(url) securityGlyph?.text = securityGlyphFor(url) - // Reset favorite state for the new URL (we don't know if it's a favorite without a round-trip). - if (onFavoriteToggle != null) { - isFavorite = false - favoriteLabel?.text = context.getString(R.string.browser_favorite_add) - } + if (hostOf(url) != previous?.let(::hostOf)) setTitleText(hostOf(url) ?: url) + // A different page: its pin state is unknown until the host answers through [setFavorite]. Show + // "Add" meanwhile — the toggle sends the explicit target state, so even a tap in that window can + // only ever add (idempotent), never silently remove an existing pin. + if (url != previous && onFavoriteToggle != null) applyFavorite(false) } + /** + * Shows the page's own `<title>`, falling back to the current host when the page has none (WebView + * reports the URL itself as the title of an untitled document). + */ + fun updateTitle(pageTitle: String?) { + val real = pageTitle?.trim()?.takeIf { it.isNotEmpty() && it != currentUrl } + setTitleText(real ?: currentUrl?.let(::hostOf) ?: return) + } + + /** Applies the registry's answer for [url]; ignored when the user has already moved to another page. */ + fun setFavorite( + url: String, + favorite: Boolean, + ) { + if (url == currentUrl) applyFavorite(favorite) + } + + private fun applyFavorite(favorite: Boolean) { + isFavorite = favorite + favoriteLabel?.text = context.getString(if (favorite) R.string.browser_favorite_remove else R.string.browser_favorite_add) + } + + private fun setTitleText(text: String) { + title = text + titleView?.text = text + grabber?.contentDescription = text + } + + private fun hostOf(url: String): String? = runCatching { url.toUri().host }.getOrNull()?.takeIf { it.isNotBlank() } + private fun toggleFavorite() { val url = currentUrl?.takeIf { it.isNotBlank() } ?: return - isFavorite = !isFavorite - favoriteLabel?.text = context.getString(if (isFavorite) R.string.browser_favorite_remove else R.string.browser_favorite_add) + applyFavorite(!isFavorite) collapse() onFavoriteToggle?.invoke(url, isFavorite) } diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletIpc.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletIpc.kt index 8611a0d8cc..647c329cf8 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletIpc.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletIpc.kt @@ -90,10 +90,11 @@ object NappletIpc { const val MSG_RECORD_ICON = 10 /** - * Host → broker (browser mode): toggle a URL in the main-process favorites registry. Carries - * [KEY_FAVORITE_URL] and [KEY_FAVORITE_LABEL]. The broker adds the URL if it isn't already - * a favorite, or removes it if it is — identical to the in-app star toggle on the home screen. - * Fire-and-forget; no reply needed. + * Host → broker (browser mode): add or remove a URL in the main-process favorites registry. Carries + * [KEY_FAVORITE_URL], [KEY_FAVORITE_LABEL] and [KEY_FAVORITE_IS_FAVORITE] (the state the user asked + * for). The explicit target matters: a blind flip against a stale chrome would remove a pin the user + * meant to add. Without [KEY_FAVORITE_IS_FAVORITE] the broker falls back to flipping the current + * state. When `replyTo` is set, the broker answers with [MSG_WEB_FAVORITE_STATE]. */ const val MSG_TOGGLE_WEB_FAVORITE = 11 @@ -120,6 +121,19 @@ object NappletIpc { */ const val MSG_RELEASE_CLIENT = 13 + /** + * Host → broker (browser mode): is [KEY_FAVORITE_URL] a favorite? Sent whenever the displayed page + * changes, so the star reflects the registry instead of guessing. The broker answers `replyTo` with + * [MSG_WEB_FAVORITE_STATE]; it keeps no reference to the Messenger. + */ + const val MSG_QUERY_WEB_FAVORITE = 14 + + /** + * Broker → host: the favorite state of [KEY_FAVORITE_URL] ([KEY_FAVORITE_IS_FAVORITE]). The reply to + * [MSG_QUERY_WEB_FAVORITE], and to a [MSG_TOGGLE_WEB_FAVORITE] that carried a `replyTo`. + */ + const val MSG_WEB_FAVORITE_STATE = 15 + const val KEY_REQUEST_ID = "requestId" const val KEY_PAYLOAD = "payload" @@ -144,6 +158,9 @@ object NappletIpc { /** A human-readable label for the favorited URL (typically the host). */ const val KEY_FAVORITE_LABEL = "favoriteLabel" + /** Boolean: whether [KEY_FAVORITE_URL] is (or should become) a favorite. */ + const val KEY_FAVORITE_IS_FAVORITE = "favoriteIsFavorite" + /** Boolean: this sandbox surface is now foreground (true) or backgrounded (false). */ const val KEY_FOREGROUND = "foreground" diff --git a/nappletHost/src/main/res/values/strings.xml b/nappletHost/src/main/res/values/strings.xml index 34c48f60bf..2bc1944e43 100644 --- a/nappletHost/src/main/res/values/strings.xml +++ b/nappletHost/src/main/res/values/strings.xml @@ -36,4 +36,12 @@ <!-- Developer console: page-load failures surfaced as console errors --> <string name="napplet_console_load_error">Failed to load (%1$d): %2$s</string> <string name="napplet_console_http_error">HTTP %1$d %2$s</string> + + <!-- Page-originated JavaScript dialogs (alert / confirm / prompt / beforeunload) in the browser --> + <string name="napplet_js_dialog_title">%1$s says</string> + <string name="napplet_js_dialog_title_generic">This page says</string> + <string name="napplet_js_dialog_block">Block dialogs from this page</string> + <string name="napplet_js_leave_title">Leave site?</string> + <string name="napplet_js_leave_message">Changes you made may not be saved.</string> + <string name="napplet_js_leave">Leave</string> </resources> From 1770ef27c5b5a959c9cba53415b7fbf5f407d2ed Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 15:52:53 +0000 Subject: [PATCH 04/60] fix(quartz): give each addressable tagging target its own d MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EventTagging's deterministic `d` is `event-tag-<tagSlug>-<target8>-<asserter8>`, and for an addressable target `<target8>` was `address.pubKeyHex.take(8)` — the author segment of the coordinate, which repeats across everything that author writes. Two of the same author's addressable events, tagged with the same tag by the same asserter, therefore produced the same `d`. Kind 39999 is addressable, so those two assertions do not sit side by side: the later one silently replaces the earlier, and the first tagging disappears. `<target8>` is now the first 8 hex of sha256 over the whole coordinate, so all three segments discriminate. `ByEventId` is unchanged — an event id already covers the whole event, and hashing it would only cost a round of SHA-256 per assertion. The `d` stays deterministic, which is what lets a re-tag replace its own assertion rather than pile up a second one, and nothing parses the `d` back into a target: it is read only as an identity, and the target itself is in the `a`/`e` tag. Nothing published depends on the old format — `EventTagging` has no caller outside quartz's own tests yet. Regressions pinned in AuditRegressionTest, each failing first: two targets differing only by d-tag, only by kind, and only by author. The derivation is pinned against SHA-256 prefixes computed outside this codebase, so the tests cross-check it rather than restate it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXvKXakvup4inNFfAhhr4L --- .../taggings/EventTagging.kt | 14 ++++- .../decentralizedLists/AuditRegressionTest.kt | 61 +++++++++++++++++++ .../TapestryExtensionsTest.kt | 4 +- 3 files changed, 75 insertions(+), 4 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/EventTagging.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/EventTagging.kt index 1e157b23e6..782ad80571 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/EventTagging.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/EventTagging.kt @@ -32,22 +32,30 @@ import com.vitorpamplona.quartz.experimental.decentralizedLists.taggings.tags.Po import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.fastFirstNotNullOfOrNull +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.utils.sha256.sha256 /** What an event tagging is about: an addressable event (`a`) or a plain one (`e`). */ @Immutable sealed interface TaggingTarget { - /** The first 8 characters used in the assertion `d`. */ + /** + * The 8 hex characters that stand for this target in the assertion `d`. Two distinct targets + * must not produce the same one: kind 39999 is addressable, so taggings that share a `d` do + * not sit side by side — the later one replaces the earlier. + */ val prefix: String @Immutable data class ByAddress( val address: Address, ) : TaggingTarget { - // the author segment of the coordinate, not its kind - override val prefix get() = address.pubKeyHex.take(8) + // Hashed over the WHOLE coordinate, because no single segment identifies the target: the + // author repeats across everything they write, and the kind across everything of a type. + // A plain event can use its own id (below) since that already covers the whole event. + override val prefix get() = sha256(address.toValue().encodeToByteArray()).toHexKey().take(8) } @Immutable diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/AuditRegressionTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/AuditRegressionTest.kt index ab0579b6df..e1f2f2c56a 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/AuditRegressionTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/AuditRegressionTest.kt @@ -36,6 +36,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertIs +import kotlin.test.assertNotEquals /** Regressions found in the audit: each failed before its fix. */ class AuditRegressionTest { @@ -111,6 +112,66 @@ class AuditRegressionTest { assertFalse(name in node.linkedAddressIds()) } + // The `d` must separate two addressable targets. kind 39999 is addressable, so a colliding + // `d` means the second tagging silently replaces the first. + @Test + fun twoAddressableTargetsByTheSameAuthorGetDifferentDTags() { + val first = TaggingTarget.ByAddress(Address(39999, bob, "good-tag")) + val second = TaggingTarget.ByAddress(Address(39999, bob, "other-tag")) + + assertNotEquals( + EventTagging.dTag("awesome-tag", first, alice), + EventTagging.dTag("awesome-tag", second, alice), + ) + } + + // Same idea across the other two coordinate segments: only one of the three may differ. + @Test + fun addressableTargetsDifferingOnlyByKindGetDifferentDTags() { + assertNotEquals( + EventTagging.dTag("awesome-tag", TaggingTarget.ByAddress(Address(39999, bob, "tag")), alice), + EventTagging.dTag("awesome-tag", TaggingTarget.ByAddress(Address(30023, bob, "tag")), alice), + ) + } + + @Test + fun addressableTargetsDifferingOnlyByAuthorGetDifferentDTags() { + assertNotEquals( + EventTagging.dTag("awesome-tag", TaggingTarget.ByAddress(Address(39999, bob, "tag")), alice), + EventTagging.dTag("awesome-tag", TaggingTarget.ByAddress(Address(39999, other, "tag")), alice), + ) + } + + // ...while the `d` stays deterministic, which is what lets a re-tag replace its own assertion + // instead of piling up a second one. Pinned against SHA-256 prefixes of the coordinates + // computed outside this codebase, so the test cross-checks the derivation rather than + // restating it: a change of hash input would silently orphan every assertion already signed. + @Test + fun theAddressablePrefixIsTheHashOfTheWholeCoordinate() { + assertEquals( + "event-tag-awesome-tag-6a5e1c40-aaaaaaaa", + EventTagging.dTag("awesome-tag", TaggingTarget.ByAddress(Address(39999, bob, "good-tag")), alice), + ) + assertEquals( + "event-tag-awesome-tag-e89b797c-aaaaaaaa", + EventTagging.dTag("awesome-tag", TaggingTarget.ByAddress(Address(39999, bob, "other-tag")), alice), + ) + assertEquals( + "event-tag-awesome-tag-96856fd8-aaaaaaaa", + EventTagging.dTag("awesome-tag", TaggingTarget.ByAddress(Address(30023, bob, "good-tag")), alice), + ) + } + + // A plain event is still named by its own id: it already covers the whole event, and hashing + // it would only cost a round of SHA-256 per assertion built. + @Test + fun plainEventTargetsStillUseTheirOwnId() { + assertEquals( + "event-tag-awesome-tag-11111111-aaaaaaaa", + EventTagging.dTag("awesome-tag", TaggingTarget.ByEventId("1".repeat(64)), alice), + ) + } + // Authored JSON: an explicit null in a list field must not make the whole section unreadable. @Test fun explicitNullListsInTheWordSectionAreTolerated() { diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/TapestryExtensionsTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/TapestryExtensionsTest.kt index 8e4b8a72a6..c212e8deb2 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/TapestryExtensionsTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/TapestryExtensionsTest.kt @@ -247,7 +247,9 @@ class TapestryExtensionsTest { assertEquals( listOf( - listOf("d", "event-tag-awesome-tag-cccccccc-aaaaaaaa"), + // 4d7a80b1 is sha256("39999:${"c".repeat(64)}:good-tag").take(8) — the whole + // coordinate, so two of the assistant's tags cannot share this `d`. + listOf("d", "event-tag-awesome-tag-4d7a80b1-aaaaaaaa"), listOf("z", "39998:$ta:nostr-event-tag"), listOf("z", "39999:$bob:tagging:awesome-tag-tagging"), listOf("a", "39999:$assistant:good-tag"), From 68079cf3cc8f5df95d78b8b61dc24697d5dc22ee Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 16:15:34 +0000 Subject: [PATCH 05/60] feat(browser): shared PWA-style chrome spec and site-permission model BrowserChrome decides which actions the browser top pill shows and in what order for every surface (web/nsite/napplet x embedded/full screen), plus security badge, scope check, text-zoom steps, desktop user agent and theme-color parsing (BrowserChromeTest). BrowserSitePermission names camera/microphone/location across processes. Shared labels live in the commons Android strings; FormatSize, DesktopWindows and AddToHomeScreen glyphs added to the Material Symbols subset font. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TEkj7Eo2xbidVHAoF8GZKQ --- .../src/androidMain/res/values/strings.xml | 92 ++++++ .../amethyst/commons/browser/BrowserChrome.kt | 261 ++++++++++++++++++ .../commons/browser/BrowserSitePermission.kt | 43 +++ .../commons/browser/BrowserChromeTest.kt | 175 ++++++++++++ .../font/material_symbols_outlined.ttf | Bin 493756 -> 498440 bytes .../commons/icons/symbols/MaterialSymbols.kt | 3 + 6 files changed, 574 insertions(+) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/BrowserChrome.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/BrowserSitePermission.kt create mode 100644 commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/browser/BrowserChromeTest.kt diff --git a/commons/src/androidMain/res/values/strings.xml b/commons/src/androidMain/res/values/strings.xml index 35d0cd1366..6e673d6fda 100644 --- a/commons/src/androidMain/res/values/strings.xml +++ b/commons/src/androidMain/res/values/strings.xml @@ -9,4 +9,96 @@ <string name="browser_file_chooser_title">Choose a file</string> <!-- Shown when the device has no app that can hand a file back to the page. --> <string name="browser_file_chooser_unavailable">No app available to pick a file</string> + <!-- Browser top pill (shared by the embedded tab and the full-screen browser) --> + <string name="browser_action_back">Back</string> + <string name="browser_action_forward">Forward</string> + <string name="browser_action_reload">Reload</string> + <string name="browser_action_stop">Stop loading</string> + <string name="browser_action_favorite_add">Add to favorites</string> + <string name="browser_action_favorite_remove">Remove from favorites</string> + <string name="browser_action_share">Share…</string> + <string name="browser_action_back_to_app">Back to %1$s</string> + <string name="browser_action_copy_link">Copy link</string> + <string name="browser_action_edit_address">Edit address</string> + <string name="browser_action_find_in_page">Find in page</string> + <string name="browser_action_text_size">Text size</string> + <string name="browser_action_text_size_value">%1$d%%</string> + <string name="browser_action_text_smaller">Smaller text</string> + <string name="browser_action_text_larger">Larger text</string> + <string name="browser_action_desktop_site">Desktop site</string> + <string name="browser_action_add_to_home">Add to Home screen</string> + <string name="browser_action_open_in_browser_app">Open in another browser</string> + <string name="browser_action_open_full_screen">Open full screen</string> + <string name="browser_action_tor_on">Loads over Tor</string> + <string name="browser_action_tor_off">Loads over the open web</string> + <string name="browser_action_access_info">What it can access</string> + <string name="browser_action_site_settings">Site settings & permissions</string> + <string name="browser_action_close">Close</string> + <string name="browser_section_privacy">Privacy</string> + <string name="browser_section_developer">Developer</string> + <string name="browser_security_tor">Onion-routed over Tor</string> + <string name="browser_security_https">Secure connection</string> + <string name="browser_security_http">Not secure</string> + <string name="browser_security_sandbox">Sandboxed app</string> + <string name="browser_link_copied">Link copied</string> + <string name="browser_no_other_browser">No other browser installed</string> + + <!-- Find in page --> + <string name="browser_find_hint">Find in page</string> + <string name="browser_find_count">%1$d/%2$d</string> + <string name="browser_find_previous">Previous match</string> + <string name="browser_find_next">Next match</string> + <string name="browser_find_close">Close find in page</string> + + <!-- Page-originated JavaScript dialogs (alert / confirm / prompt / beforeunload) --> + <string name="browser_js_dialog_title">%1$s says</string> + <string name="browser_js_dialog_title_generic">This page says</string> + <string name="browser_js_dialog_block">Block dialogs from this page</string> + <string name="browser_js_leave_title">Leave site?</string> + <string name="browser_js_leave_message">Changes you made may not be saved.</string> + <string name="browser_js_leave">Leave</string> + + <!-- Downloads and sharing --> + <string name="browser_download_started">Downloading %1$s…</string> + <string name="browser_download_saved">Saved %1$s to Downloads</string> + <string name="browser_download_failed">Couldn\'t download %1$s</string> + <string name="browser_share_chooser">Share</string> + + <!-- Page info --> + <string name="browser_page_info_title">Page info</string> + <string name="browser_page_info_https">Your connection to this site is encrypted.</string> + <string name="browser_page_info_http">Your connection to this site is not encrypted. Don\'t enter passwords or payment details.</string> + <string name="browser_page_info_tor">This site loads over Tor. The site can\'t see your IP address.</string> + <string name="browser_page_info_open_web">This site loads over the open web. The site can see your IP address.</string> + <string name="browser_page_info_certificate">Certificate issued to %1$s by %2$s, valid until %3$s.</string> + <string name="browser_page_info_permissions">Permissions</string> + <string name="browser_page_info_clear_data">Clear site data</string> + <string name="browser_site_data_cleared">Site data cleared</string> + + <!-- Site permissions (camera / microphone / location) --> + <string name="browser_permission_title">%1$s wants to</string> + <string name="browser_permission_camera">Use your camera</string> + <string name="browser_permission_microphone">Use your microphone</string> + <string name="browser_permission_location">Know your approximate location</string> + <string name="browser_permission_allow">Allow</string> + <string name="browser_permission_block">Block</string> + <string name="browser_permission_system_denied">Amethyst doesn\'t have Android\'s permission for this. Allow it in system settings.</string> + <string name="browser_permission_ask">Ask</string> + <string name="browser_permission_allowed">Allowed</string> + <string name="browser_permission_blocked">Blocked</string> + <string name="browser_permission_section">Site permissions</string> + + <!-- Long-press menu --> + <string name="browser_ctx_open_new_window">Open in new window</string> + <string name="browser_ctx_copy_link">Copy link address</string> + <string name="browser_ctx_share_link">Share link</string> + <string name="browser_ctx_download_image">Download image</string> + <string name="browser_ctx_copy_image_link">Copy image address</string> + + <!-- Home screen shortcuts --> + <string name="browser_home_shortcut_unsupported">Your launcher doesn\'t support shortcuts</string> + + <!-- Renderer crash --> + <string name="browser_renderer_gone">This page stopped working</string> + <string name="browser_renderer_gone_reload">Reload</string> </resources> diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/BrowserChrome.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/BrowserChrome.kt new file mode 100644 index 0000000000..a5f8790c71 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/BrowserChrome.kt @@ -0,0 +1,261 @@ +/* + * 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.commons.browser + +/** + * The single description of a running web surface's controls — the top pull-down "pill" drawn over every + * embedded tab (Compose, in the main process) and every full-screen browser window (plain Views, in the + * keyless `:napplet` process). Both renderers ask this object *which* actions to show and in *what* order, + * so the two can no longer drift; they only decide how each action looks. + * + * Modelled on an installed Chrome PWA's app menu: a header naming the page and its origin (with its + * security state), a row of icon buttons (back · forward · reload/stop · star · share), then the menu rows, + * with the privacy rows and the developer console at the end. + */ +object BrowserChrome { + /** What is being shown: a live website, a verified NIP-5A nsite, or a sandboxed NIP-5D napplet. */ + enum class Surface { WEB, NSITE, NAPPLET } + + /** Where it is shown: an in-app tab over the bottom bar, or its own full-screen window/task. */ + enum class Presentation { EMBEDDED, FULL_SCREEN } + + /** The connection badge on the origin chip. */ + enum class Security { TOR, HTTPS, HTTP, SANDBOX } + + /** Every control the pill can offer. Renderers map each to an icon + label. */ + enum class Action { + // Icon row + BACK, + FORWARD, + RELOAD, + STOP, + FAVORITE, + SHARE, + + // Menu rows + BACK_TO_APP, + COPY_LINK, + EDIT_ADDRESS, + FIND_IN_PAGE, + TEXT_SIZE, + DESKTOP_SITE, + ADD_TO_HOME_SCREEN, + OPEN_IN_BROWSER_APP, + OPEN_FULL_SCREEN, + + // Privacy + TOR, + ACCESS_INFO, + SITE_SETTINGS, + + // Developer + CONSOLE, + } + + /** A group of menu rows; renderers draw a divider (and, for PRIVACY/DEVELOPER, a label) above each. */ + data class Section( + val kind: SectionKind, + val actions: List<Action>, + ) + + enum class SectionKind { PAGE, PRIVACY, DEVELOPER } + + /** Everything the menu layout depends on. Pure data, so the layout is testable off-device. */ + data class State( + val surface: Surface, + val presentation: Presentation, + /** The page on screen now. */ + val url: String, + /** The page this app/tab was opened with — its "scope", for the back-to-app action. */ + val startUrl: String, + val canGoBack: Boolean = false, + val canGoForward: Boolean = false, + val isLoading: Boolean = false, + /** Tor routing state, or null when this surface offers no Tor choice. */ + val torOn: Boolean? = null, + /** Whether the star is offered at all. */ + val canFavorite: Boolean = true, + /** Whether an editable permissions screen exists for this surface. */ + val hasSiteSettings: Boolean = true, + /** Whether a "what it can access" summary exists (sandboxed surfaces). */ + val hasAccessInfo: Boolean = false, + /** Whether the console can be shown. */ + val hasConsole: Boolean = true, + /** Whether this surface can search its page. */ + val hasFind: Boolean = true, + /** Whether this surface can resize its text. */ + val hasTextSize: Boolean = true, + ) { + val isSandbox: Boolean get() = surface != Surface.WEB + } + + /** + * The icon row, left to right. A live website gets Chrome's full row. Sandboxed apps are served from + * internal, verified content, so there is nothing meaningful to share or step through; they get reload + * and the star. + */ + fun iconRow(state: State): List<Action> = + buildList { + if (!state.isSandbox) { + add(Action.BACK) + add(Action.FORWARD) + } + add(if (state.isLoading) Action.STOP else Action.RELOAD) + if (state.canFavorite) add(Action.FAVORITE) + if (!state.isSandbox) add(Action.SHARE) + } + + /** Whether an icon-row action is currently usable (back/forward follow the page history). */ + fun isEnabled( + state: State, + action: Action, + ): Boolean = + when (action) { + Action.BACK -> state.canGoBack + Action.FORWARD -> state.canGoForward + else -> true + } + + /** The menu rows under the icon row, grouped. Empty groups are dropped. */ + fun sections(state: State): List<Section> { + val web = !state.isSandbox + val page = + buildList { + if (web && isOutOfScope(state.url, state.startUrl)) add(Action.BACK_TO_APP) + if (web) add(Action.COPY_LINK) + if (web) add(Action.EDIT_ADDRESS) + if (state.hasFind) add(Action.FIND_IN_PAGE) + if (state.hasTextSize) add(Action.TEXT_SIZE) + if (web) add(Action.DESKTOP_SITE) + if (web) add(Action.ADD_TO_HOME_SCREEN) + if (web) add(Action.OPEN_IN_BROWSER_APP) + if (state.presentation == Presentation.EMBEDDED) add(Action.OPEN_FULL_SCREEN) + } + val privacy = + buildList { + if (state.torOn != null) add(Action.TOR) + if (state.hasAccessInfo) add(Action.ACCESS_INFO) + if (state.hasSiteSettings) add(Action.SITE_SETTINGS) + } + val developer = if (state.hasConsole) listOf(Action.CONSOLE) else emptyList() + return listOf( + Section(SectionKind.PAGE, page), + Section(SectionKind.PRIVACY, privacy), + Section(SectionKind.DEVELOPER, developer), + ).filter { it.actions.isNotEmpty() } + } + + /** The badge for the origin chip. */ + fun security(state: State): Security = + when { + state.isSandbox -> Security.SANDBOX + state.torOn == true -> Security.TOR + state.url.startsWith("https://", ignoreCase = true) -> Security.HTTPS + else -> Security.HTTP + } + + /** + * True when the page on screen left the app's origin — the case where Chrome shows its out-of-scope + * bar. Blank pages and unparseable URLs are never "out of scope". + */ + fun isOutOfScope( + url: String, + startUrl: String, + ): Boolean { + val here = originOf(url) ?: return false + val home = originOf(startUrl) ?: return false + return !here.equals(home, ignoreCase = true) + } + + /** + * `scheme://host[:port]` of an http(s) [url], lowercased, or null for anything else (about:, data:, + * blank). The same shape the WebView reports as a page origin. + */ + fun originOf(url: String): String? { + val schemeEnd = url.indexOf("://") + if (schemeEnd <= 0) return null + val scheme = url.substring(0, schemeEnd).lowercase() + if (scheme != "http" && scheme != "https") return null + val host = OmniboxInput.hostOf(url)?.lowercase()?.takeIf { it.isNotEmpty() } ?: return null + val authority = + url + .substring(schemeEnd + 3) + .substringBefore('/') + .substringBefore('?') + .substringBefore('#') + .substringAfterLast('@') + val port = + authority + .substringAfterLast(']', authority) + .substringAfter(':', "") + .toIntOrNull() + ?.takeIf { !(scheme == "https" && it == 443) && !(scheme == "http" && it == 80) } + return "$scheme://$host" + (port?.let { ":$it" } ?: "") + } + + /** The host shown on the origin chip, or the raw URL when it has none. */ + fun displayHost(url: String): String = OmniboxInput.hostOf(url)?.takeIf { it.isNotEmpty() } ?: url + + /** Text-size steps offered by the TEXT_SIZE row, in percent (Chrome's accessibility range, coarser). */ + val TEXT_ZOOM_STEPS = listOf(75, 90, 100, 115, 130, 150, 175, 200) + + const val DEFAULT_TEXT_ZOOM = 100 + + /** The next larger (or smaller, with [larger] = false) step from [current], clamped to the range. */ + fun stepTextZoom( + current: Int, + larger: Boolean, + ): Int = + if (larger) { + TEXT_ZOOM_STEPS.firstOrNull { it > current } ?: TEXT_ZOOM_STEPS.last() + } else { + TEXT_ZOOM_STEPS.lastOrNull { it < current } ?: TEXT_ZOOM_STEPS.first() + } + + /** + * Parses a computed CSS colour (`rgb(r, g, b)` / `rgba(r, g, b, a)`, what `getComputedStyle` returns) + * into an opaque `0xFFRRGGBB`. Null for anything else, or a colour more than half transparent (a page + * that "clears" its theme colour that way should get the default bars back). + */ + fun parseCssRgb(css: String?): Int? { + val match = CSS_RGB.matchEntire(css?.trim() ?: return null) ?: return null + val (r, g, b) = match.destructured.let { (r, g, b, _) -> Triple(r.toInt(), g.toInt(), b.toInt()) } + if (r > 255 || g > 255 || b > 255) return null + val alpha = match.groupValues[4].takeIf { it.isNotEmpty() }?.toFloatOrNull() ?: 1f + if (alpha < 0.5f) return null + return (0xFF shl 24) or (r shl 16) or (g shl 8) or b + } + + private val CSS_RGB = Regex("""rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*([0-9.]+)\s*)?\)""") + + /** + * The "desktop site" user agent for [mobileUserAgent]: Chrome's own trick — swap the Android platform + * token for a Linux desktop one and drop the `Mobile` and WebView (`; wv`) markers, so servers that sniff + * the UA send their desktop layout. Leaves the Chrome/WebKit version tokens intact. + */ + fun desktopUserAgent(mobileUserAgent: String): String = + mobileUserAgent + .replace(Regex("""\(Linux; Android[^)]*\)"""), "(X11; Linux x86_64)") + .replace("; wv)", ")") + .replace(" Mobile Safari/", " Safari/") + .replace(Regex("""\s+Mobile(?=\s|$)"""), "") + .replace(Regex("""\s+Version/\d+(\.\d+)*"""), "") +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/BrowserSitePermission.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/BrowserSitePermission.kt new file mode 100644 index 0000000000..59b6870e53 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/BrowserSitePermission.kt @@ -0,0 +1,43 @@ +/* + * 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.commons.browser + +/** + * The powerful web features a site must ask for before using, as Chrome's site settings list them. Shared + * by the keyless `:napplet` browser (which receives the page's request) and the main process (which + * remembers the user's answer per origin), so both name them the same way over IPC. + */ +enum class BrowserSitePermission( + /** Stable wire/storage name. Never rename: it is persisted. */ + val key: String, +) { + CAMERA("camera"), + MICROPHONE("microphone"), + LOCATION("location"), + ; + + /** The user's remembered answer for one permission on one origin. [ASK] = never answered. */ + enum class Decision { ASK, ALLOW, BLOCK } + + companion object { + fun fromKey(key: String?): BrowserSitePermission? = entries.firstOrNull { it.key == key } + } +} diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/browser/BrowserChromeTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/browser/BrowserChromeTest.kt new file mode 100644 index 0000000000..5bf3268972 --- /dev/null +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/browser/BrowserChromeTest.kt @@ -0,0 +1,175 @@ +/* + * 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.commons.browser + +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.Action +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.Presentation +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.SectionKind +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.Security +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.State +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.Surface +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class BrowserChromeTest { + private fun web( + presentation: Presentation = Presentation.FULL_SCREEN, + url: String = "https://example.com/a", + startUrl: String = "https://example.com/", + torOn: Boolean? = null, + isLoading: Boolean = false, + ) = State(Surface.WEB, presentation, url, startUrl, isLoading = isLoading, torOn = torOn) + + @Test + fun webIconRowMirrorsChrome() { + assertEquals(listOf(Action.BACK, Action.FORWARD, Action.RELOAD, Action.FAVORITE, Action.SHARE), BrowserChrome.iconRow(web())) + } + + @Test + fun reloadBecomesStopWhileLoading() { + assertEquals(Action.STOP, BrowserChrome.iconRow(web(isLoading = true))[2]) + } + + @Test + fun sandboxIconRowHasNoHistoryOrShare() { + val state = State(Surface.NAPPLET, Presentation.EMBEDDED, "https://x.napplet.local/", "https://x.napplet.local/") + assertEquals(listOf(Action.RELOAD, Action.FAVORITE), BrowserChrome.iconRow(state)) + } + + @Test + fun backAndForwardFollowHistory() { + val state = web().copy(canGoBack = true, canGoForward = false) + assertTrue(BrowserChrome.isEnabled(state, Action.BACK)) + assertFalse(BrowserChrome.isEnabled(state, Action.FORWARD)) + assertTrue(BrowserChrome.isEnabled(state, Action.SHARE)) + } + + @Test + fun fullScreenWebMenuOrder() { + val sections = BrowserChrome.sections(web(torOn = true)) + assertEquals(listOf(SectionKind.PAGE, SectionKind.PRIVACY, SectionKind.DEVELOPER), sections.map { it.kind }) + assertEquals( + listOf( + Action.COPY_LINK, + Action.EDIT_ADDRESS, + Action.FIND_IN_PAGE, + Action.TEXT_SIZE, + Action.DESKTOP_SITE, + Action.ADD_TO_HOME_SCREEN, + Action.OPEN_IN_BROWSER_APP, + ), + sections[0].actions, + ) + assertEquals(listOf(Action.TOR, Action.SITE_SETTINGS), sections[1].actions) + assertEquals(listOf(Action.CONSOLE), sections[2].actions) + } + + @Test + fun embeddedWebAddsOpenFullScreenLast() { + val page = BrowserChrome.sections(web(presentation = Presentation.EMBEDDED))[0].actions + assertEquals(Action.OPEN_FULL_SCREEN, page.last()) + } + + @Test + fun noTorRowWithoutTor() { + val privacy = BrowserChrome.sections(web(torOn = null)).first { it.kind == SectionKind.PRIVACY } + assertFalse(Action.TOR in privacy.actions) + } + + @Test + fun leavingTheAppOriginOffersBackToApp() { + val page = BrowserChrome.sections(web(url = "https://accounts.other.com/login"))[0].actions + assertEquals(Action.BACK_TO_APP, page.first()) + } + + @Test + fun sandboxMenuKeepsOnlyApplicableRows() { + val state = + State(Surface.NSITE, Presentation.FULL_SCREEN, "https://a.napplet.local/", "https://a.napplet.local/", torOn = false, hasAccessInfo = true) + val sections = BrowserChrome.sections(state) + assertEquals(listOf(Action.FIND_IN_PAGE, Action.TEXT_SIZE), sections[0].actions) + assertEquals(listOf(Action.TOR, Action.ACCESS_INFO, Action.SITE_SETTINGS), sections[1].actions) + } + + @Test + fun surfacesWithoutFindOrTextSizeDropThoseRows() { + val state = + State(Surface.NAPPLET, Presentation.EMBEDDED, "", "", hasFind = false, hasTextSize = false, hasAccessInfo = true) + assertEquals(listOf(Action.OPEN_FULL_SCREEN), BrowserChrome.sections(state)[0].actions) + } + + @Test + fun securityBadge() { + assertEquals(Security.HTTPS, BrowserChrome.security(web())) + assertEquals(Security.HTTP, BrowserChrome.security(web(url = "http://example.com"))) + assertEquals(Security.TOR, BrowserChrome.security(web(torOn = true))) + assertEquals(Security.SANDBOX, BrowserChrome.security(State(Surface.NAPPLET, Presentation.EMBEDDED, "x", "x"))) + } + + @Test + fun originOfNormalizes() { + assertEquals("https://example.com", BrowserChrome.originOf("https://Example.com/path?q=1")) + assertEquals("https://example.com", BrowserChrome.originOf("https://example.com:443/")) + assertEquals("http://example.com:8080", BrowserChrome.originOf("http://user@example.com:8080/x")) + assertNull(BrowserChrome.originOf("about:blank")) + assertNull(BrowserChrome.originOf("data:text/html,hi")) + } + + @Test + fun scopeIgnoresPathsAndBlankPages() { + assertFalse(BrowserChrome.isOutOfScope("https://example.com/deep/page", "https://example.com/")) + assertTrue(BrowserChrome.isOutOfScope("https://sub.example.com/", "https://example.com/")) + assertFalse(BrowserChrome.isOutOfScope("about:blank", "https://example.com/")) + } + + @Test + fun textZoomSteps() { + assertEquals(115, BrowserChrome.stepTextZoom(100, larger = true)) + assertEquals(90, BrowserChrome.stepTextZoom(100, larger = false)) + assertEquals(200, BrowserChrome.stepTextZoom(200, larger = true)) + assertEquals(75, BrowserChrome.stepTextZoom(75, larger = false)) + assertEquals(115, BrowserChrome.stepTextZoom(105, larger = true)) + } + + @Test + fun parsesComputedCssColors() { + assertEquals(0xFF0C2238.toInt(), BrowserChrome.parseCssRgb("rgb(12, 34, 56)")) + assertEquals(0xFFFFFFFF.toInt(), BrowserChrome.parseCssRgb("rgba(255, 255, 255, 0.9)")) + assertNull(BrowserChrome.parseCssRgb("rgba(0, 0, 0, 0)")) + assertNull(BrowserChrome.parseCssRgb("#ffffff")) + assertNull(BrowserChrome.parseCssRgb("")) + assertNull(BrowserChrome.parseCssRgb("rgb(300, 0, 0)")) + } + + @Test + fun desktopUserAgentDropsMobileMarkers() { + val mobile = + "Mozilla/5.0 (Linux; Android 14; Pixel 8 Build/AP1A; wv) AppleWebKit/537.36 (KHTML, like Gecko) " + + "Version/4.0 Chrome/120.0.6099.230 Mobile Safari/537.36" + assertEquals( + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.230 Safari/537.36", + BrowserChrome.desktopUserAgent(mobile), + ) + } +} diff --git a/commonsUI/src/commonMain/composeResources/font/material_symbols_outlined.ttf b/commonsUI/src/commonMain/composeResources/font/material_symbols_outlined.ttf index c9e24fb3ffd7cdce077e1a3530f878ce3ccb2856..3af0f83bd12753dca85367876b4f9743284cd835 100644 GIT binary patch delta 13438 zcmaKS3qVwL_W$|b*UT^j!@vwP3<wugL?cB7Q4~ddKSRwjE0t1H(=;<9*BW4YZMkOQ zHre)Yd*`<O+-#%%Tbn($T-P4kw*AUV39YOM5d;PphTrGh8E~xK{~hk!x%YfO=bZ2P zp2zoozr1!|#$7LG<dPsF2fh?aoUv$e*(3M-eh5+fULx`M%u6OO6pzzjqTtl~ax;9v zO=$a0#ny_=Wcz+UQD*_}%dg&0vFU%`T8UO_I+4k_8uWWwH@pD)ZJ;+^Q*i_162bLB zT+P?4zj<}iukS4)vR_Pe<nT3Uzqb4n8&TIL()K>EW>v+?v3D)s3=P1K#;m~uQ-djs zd?Wy%L2EW_sl4*3%j3}?NF)zhzwzpd@WwG85!v$jenUm&Ch@ckg+wLV>(^CmShaM1 z^&z4!b<k~_Hr}ws_W9eN5gpGXa<4N?AO-(@U)-#3mfI(^nI-ukaP;lJ?kzE{U)}!= zUu&m|apJxMw~9A~y`0LaGWt10hia$GZyzuz2_jQMLm8(N-)veZDJERRZrqq?ckLK; zs`u+~!;prt4U-zm8m2dtHymtuyWzctFHV?G*bdyUZWm0?G3b*UrpC}spy!y1gfBfS zD=S@}nU#^{(%&G;f+dJDPi3Ce)Aezgb2I<0Ka=@JW@ctmW=ZDg%x5$2&va!j$~cqp zyNr7>{TbJ0{wZU0#y+Bqk1`Htyo~GQjEx!V2E0CCJ<)&%aGyKi;q=b*>(Xn}Uu4ed zWAWUHYifE;`qK0XzVu7e7o}}W&n8OCPUG)^Y3vD<=18-9yV88#IbN?f&3nizymP%b zdM9`@Q(sQ4A@UTb!r-Y%o{v3S+>g3@7~^@^{itWU`+oeon(y6TxIb}!<G#zi$n&gc zk0;slNB2M7X`njXGd!z3m!%v{@%_tvt$Vk7w)=~eS5v-oFLM_Yr8KxbZkL-<(4Uko z?#z_;-B~F&r8K#dQzp5pT_M-f6mLqI^OUpMHO}>zYnJm~M{?r(j_VyCCTWR=U<aJd ze!^k5cP4u5@7j05W1&=HynTiJPEbzSyX<EBiiEckw&&SBcyO70asrHRQ>{0e)|>Yl zgUua?9^wuR`rTq@t;3pEyI&{^#fk_;T+!DA4K9hCF;B|HLOhF}=brN~G1d4kGrmj3 z>{>@$MvfS6+`_JXhZrPs&;15$kjSb1y-1Nn5)cF8gMCC?o1^NPd<W?*+`k(`BYF*Y zeDd*1<kJ8BeDv3|{ZEJ{iHTyqD5rIzP!x$WQA)dnUD$+G#L=U)mm26RI!5(kw3tK> z(Ml?$BD#v6q!y9Bl8(|T`ckO$H65pa(<k&9mC`@xeM+S~vQaO>r;Ub*t@Kx#NiWiV zYNu9FDYnsQA;cz8LnCQ|sG>@dBAjA1y+rBslbA(KVhz1V4q8CZ)4l`gwi&`#KTH`` zHf;5<Cx;!nAo+r67rd0G<t@$&<b8c%;)QE2eDcE1;RVAV9DZ_yJi<AmWW=HoJ4U=Z zqA@=ye@_0o{M!5j`CsL?jvO)a(vi1}{C-sSs2!u;9ZjQ$jrNWHOTqerU}0h5HH8lq zzEOB`O#YY+V;(P3i>@!az373WXNq1a`g_sQq91)lp|S3<YsWq~_QkRP9@{uh9+x(5 z)VKxXel_l?aUYCpEcO)7FRm)ySA1f;XZ+>kUmPErFlfU12~SOUf5PzzXG)Swrj;x# zSzfZGWM|16CCwAFCyt%Cc;e=XZ%u5SG;Y$iNsmo>sdPo@!P550X_FUC-Zpvv<j=}- z%jTDDFMGf2>=e(G@l$-|Q(m1)Q!A$Ko0dOq)3nE?eL5{X{i5l2O#g0%bH?>E9-Hyy zOxMf>Gq=xtdFH3Ha%NpN>$}+_XWu;g>Dgb+QRZaMSvu#gIY;N}bGOZXeeU=3#?QNT z-t+VRHt+j+z4LAJbLWqrf64r{^LNjGcK+M*KbrsTMbj_(ZMnI8UHSg<uNF8KELu># zP+zfd)56fAb&Ec}*mbe*;*S>(UA$oNYm3ibvfvWmB_}WSUi#3bKP=g{<m1a$U-seB zX-jWj`ufrzFJE=}t5--@jJV>qEB<!n<STby`Q5Ui%N|`8T)t}gzUAMo$X@Yu1y#(h zc(~%kRfATqIxyBgW6@BV6STZZo=I*R0zad{g`*1xX6F<-3dW4i8kOR5IJJQTU87yK zD<0K-qO`m@Ys82wsfxdw%fq@<)l*ejUOpmA?BYk^O<5z#%bUfn@FqSH?dRD4SJpr0 z`U`$sbx}p+1GisQBZeq+h;n6JelV(XDU^;;*p-svayd0q?TYcv+D(rp`ihEN=#p!+ zNqAUARx~LkC5e?~9k*;RZ(g%K7oGZu$=gjC$>qrzrto_7MEa;FpU*G1uW2sdP9(*M z*8j6uwwE|h;&~!Z{>&yaFJTbY$JPJ-d~VqA1oLpvln1{P+y20F5$S1JFHWIhzu zE5(}1y^^`IvL`4?D=WhX=|Ili-(Pef{^%|t7tyHNy<a?GT%Z49Qtj<uzJ$75{FO(J zlYS%Zl<t*wN%tLCSU=CK+gGl-;o2=5H!Z(u?R6_R-gJZWXZN)aof?wo+OYAewd+?c zuh_hK<4wzN+_Zf2+H2Nq@&3oB2VOh%o?^4E-ne-~#g^qatlhTifd8j`n%G=B{OowK zyLR2#|M9&=?;z>?-AG>_(x=po9GEDQkeE`0M|i~mp^I#!<so9IxIkPeMu?HfiJUXX ziSeQonRA+$A!doWoK3|-ak02uEE6llO0in35$nWyaUD|LX0b(V<rFBki(5sNs1dhC zicszDwgqC>fp6O!l4Xh5#ScyTi@|#(-(HfQe4eCd*ORnw8%Zw|leB*(Nq>Htq*u)( zy*;0#Ll2VlA=>@xBuSqwB<YJmB-NLbbmC)@PT^T&DM@WjBz3()QqOFX!uceNM3SZX zB%3ZKi~z|A4J13VNp?L<vU@7Y-mxU7r;zN+e3oSWU6Kb^kv!xe$wOZux!`e<$6iA6 z<fSA}(@386BFS?BUbKSbOPWZ&S|@qkR+6tn+s!#7-?*FPo6<?He46B2za_cmOOkJ2 zLh_E6NUj}D@@_B5zcu4*A^CxNk^}FO{NR3)_uNlD`JqaZe+Q;}q2UuJNq(}6<Y#Oo zKU+rfbK6MX2jPGCf#g4e*?tIqaR<qNP9nLENPcxL$*<x5ugxUCiT3YoA^AOE4!=P1 z2S-T$80ROON&f5ulD~MF<gZSVeC#O6$MO7Ybo}IeAHF^&`5W~1+hUTx+eh+`FkKUR zcLq#bQ%F8Lk>s{DB!^x?YQRT1M2clDDYh<B;y)&3Kt3r0pCu*tZc>J=BxS@YQbu8p zIc5zhWAS_PL{g?4A!Yg&QszHI%7Ow?7A2Ch7|$+2kyn=E$4U^de}$AAL!@lmMv5<w zl<H4Nx#JE}er+aYS2iiNVDv~8DSMwKWgpu9@enBoW|Q*r!=(HriImqN>`k0+H<NOB zH!1JmO3G*ZN%?9jDGeGaCqE<QyL3{%-$}}k7n5?j336J%taA-1Jui?de@UvPoK%~a z)Z{g!y6+}6Z8hE@wvn2VO=@O4srvn-X1`48z{g1)jBC#Gqz?U-)M2>hrQoa~^}>fq z9bG``SiFBsDkF8;U8K%$Aa%idQZK%O)TO6Lz4Ad)*B>LFy4gnRulAA(r%`u6;I1c0 zeF)4RYbN!Xlces0LI;kLTDOwaH!4YeYb>dU@%#fk|9lCl$8II{2M?*I!L)lJX<`Ry z_UWW0Cz9qFPMY@<(sBw&yP%A;QAbEC#2d|o7Sbl;*{q`zNSi;3v_;2AyL1U@D-_aJ zf_UvZ(l&lZ+6@5Qw2QQ=5NUTlK-vzpy>BLIzpW%~&(_-f@MbH~LCm*G`BVGH@L*%& z&vyrLQtk0@u1Kgg_x}FE1d7ugWYImiI;lXn&=g&xBHc{6q|zkaN`<-%nhU4>z^A=$ zs!@nN3MmETB0#1CGF?}x6p%c;(nKM3@2KJl?Pr*|z+4W@LSPmGGZ&ay=fu?Qwuu9# zEGLzq-7K`55~EIWOuIEs@wFMe7o!V{q0g1*0_(E`m{MR$qxzHrvlv|%N%p#}1B5$n zXaZ)64xCQfiuN^VzZ31ZqWvw<Wi8s<$c58h_wo?&8(Vw=WkW_9WMu2kKzV^kQ(gFJ zuiI20MynvFfn6GTa5@9~#~I`_pg$TX9??J^4)So2hl9MW)F2NB`JqzrXB(6m0OpRU z{fnSX0hAdBWd@TKR~a-HPJ3PDJn@8z_DNuu1m#CUd2XMJ_62A^49XXvy$9`+z<=r@ zG0$X4pcJ%Ef#`>p8lqD``hQEscN$1hR)KvL28mIJE5!A&Ql><Qp%_wlh>IX)dW@8n z*NEy^R!tlFStVR22C@QuQi^ZvCoR2El*E$uZto`@+%9g3kLl)OD8T+Q7YZza0#l&C zl5+|Kz-mI><9CZcm|{Rb*SZWKcAlth7&Ni&rTfKT_Rf45$c^#IhfyzzN@I^+Mk>uD zD<sQw0X!lf=F#?uAv|b(gTP+OLBoT_9ybbt*yH*J;gY9BXRJAk(Y_SzN1}ZR+K)v0 zl9=|De-N*#u{QftzhRdCNsNr`V+B~<09G7G6=1a-tY*isdg&Fh$^upO$oLywn-gu1 zL`7xj8V8<^uBD@EWp9b&rl`B-MBR1xVM9E->-@vw_p#%`aXc67IM${>JjZ@)&pn@s zP95wLAl?jiX1HKRr@<}(>?U@K4;7IRNC=C82&dt?y>3ic)Wri~jzM%;C5|>T5MG<~ zmjN(MG<XBYh8iS})9_HKNCC;MV<dHFVB&Ct<Ob7a!zEcmI$>)CpzbZYnRY{6AJpaE ztLVK77BvM@n-$K$88KOpWW>9Gxd)iLfVmW{YJk}TOka9?vQ&C19+&}8I3Ji%h~fY+ z&jPa-m_3m5ATSRDGXQc1K+e|rQfDGM$#Mon<OH@aH`GOv4ZN^Q8eR9{a_L3RE3s{M zuIg)3hc*`W+Y+!S0*exe9Y*IeaS_CFlnjU165WN<UU%|ZNsa@?9_^kDm>Zz#eNc51 zR9%SfZ32cJ$PSDh80S{W8V5C0bk7PoDn>A`%20zXHn&Pzknl6|zMcK#!#kzlGCHQj z1lI5A@5GKh(&ub%510>**a3OPf^pL5Dk!uLo#4_i1qyj!I}aq@|CIE(SvUjXKliiA zcv+fZ3jFKO(y(7VzutsLyI${CX#eZdXbU`{2vWF6EP|9$bc#*IQu;<!c~3ggSF{{a z&4vTRg>Duw#lRE;6I+a&A4mfk!{&7Y!{*JPNT_pa;33JN$dL4zcjYJ2NG_<cm>ghY zgDMA@qL?9h>vO5lkOQkZVH1|aDT}SP1ae{vsvTCd!)k--rEAU6fk=$>C-Cs8eha>K zN=jvx8d%1Itbs8P1gpVWuo$Y(L3g>dXke*<rF2@F%$1E*=ed9#F<0JZuw=*C)Fxf? z|0kDdfyt6Q4B08h$B~I5CBH8@W}^3)UZah@Znq>aVrw}dG8#9;)|v-3xi)2CRM=Xq zCXXanOs84?6DMDm(KnKLz;HR?X+<6|Jo*ugy{;)<9?5#gv=8Jt<uqZho9UE$6^Ks& zD^3pyF#I5}Dgi6bfP=tl8dwd2x+aKEfcPz`@&*oL#;{Z9uu@Zo%=t_QIU!TNS{<A~ ziD1RPQUF#w_sRgP31F20Rw-bWh_(+5lCxxpH&KL@34_E5cp)&0ftjnTm;gmel^Gbk zHP@XSBLB(+@iCad`VoeBChr&_-^rb2?KrwI1q|q83~~}AmX47xW4B;iBt*z4fRQL* z+i}Wg+vOYMW6Tii-kl)N;skDimTV*o5=Q!LgQW!$C(M?Q^ANJcRFK(*T&Sm#1}8S- zsv;;OX*nTk!<pssViU^5qnFB=u-hQ8dFxVn8fRV390S3|16#1e_Dw|DVVTWHctR4` zaONmjCI7Dp7%wo{z<8sWb+F%Rh~~Jt0~ntCd4cf)Gj@agZ%$xif#KmBi{5xEjoyp} zxwujuZ3mgBS6LwQ6l>d^26>{6+1;J;3`X-rfpx%)EzoqIK^_Kl!u|69T75YfoE*Hs z<zTu#M~|aRbqU=N6sM=)bi--Sb4J7AdB}M#lmlz^r3VBE44w!~60Y&M+L9bX5_04t zNvcUQnIu^nE}5z(OI1=;jl&if=W{ya+8Z0&+t0Sg@!>FQS}54m6bxzRq);#v42B#h zzwN58@6vQ#^QLQEd{o6tQaX9PlrGV?>6Bhg>5fFRIWf^3Y;Fz)n}cRP91e>`ocaFy zGs5CX6=%emGs2q+m0}McZu)gga5lF2<+etr)o-yn8(W->d{ARe<e7|TZPg|`a5g%f zjpQxBp%3BjA$tAiM{s;z&)<4|qhD^U*PVWH)>6T3*4D;8a%1alrUEpnZ$z88#(G~P zBaAy0k8tBp^^kPFl0wLuVLi{8Gq&I{96{Szb2bk1S^HTVAD(0Q$|-I&W#KTj7U$r$ zxV5;owK!sN(TH9cwih;AjjE6*MKaZhW?!??%SV)!n_ENA2q-Q)xf7zX-v5^<>C~wj zM^|@udwaJU7pJP4+RcX})YBs=7K<YFggPWeQWWVIHjXiIYC^nhNyK51dpeqN_&R!8 zTY6+Zs$9?`*g{>cIJ&4M*xO6-E!0v?EgpCOu*s3p>l-sSRO)KS(WSBbX#I-0p;(WQ z6xCu;6-o5;tLJbeDo&>oYHSQOpAPvHKC0q-drwnyGo9|G#M5-TnodjYEeX{kp{3f~ zk`ULDzz0Ph;EwQVNoZ+Fa3o@^qAngCqeMmNu~>TKU{DUp(4nU%(bCh?!uKtFZISU8 z8HxJ(8XwHv-0ruuHxJ5i;>Z}(Z}6TT9*g!azoo0a;n-(5jy3cXySi$s;*k%$2wyM4 z*NgDYNBE9F_!c63HH57f*{=|xT8L1^+67K44c29wH6|>5k`+<dAB5J}@UzASUR7L7 z$XR29%^DkK<hI*guAka@l-hrC+4yR6{q$37tG~+dK_j}1NUFj8Z9a1Uw<z-eB66s} zPwq1~eh~zAyWQ{R5cow9*lfrAf~bmmt+C!~jry(6a9naHB*g#BGvS!6EiHbEuXg;x zFMF#g9;$SKW7PY))jt1IHO=qt2%U4kJ_qaxg*yD!SnsQf>z#eh@2u@-;D+&MdV9~r zTcW<E7`|qWi}A9!82^fk@hl2_`G`CO={|Pqkoc&P?s@i-|ET=?z8QUVCK5QG{43R$ zN^9Uy$tXr9On(&=F``T0R@cL=Ho>h_)EE_SswyIY=j2?JFek^v&q5pEYS+NkR_ZEU zrdz?I|J4TV644F?2$ex4Tg3k-i(O(@jW-qRviw0Fo&|4_qlE&ul1R0>T-MIhr<)oZ zn@*qZ?6mR`QoAiSB{fnw&a`$Q%Hwbo*WMl{TP@vcC_9S+jsc1*aPWUr%_ke2vbs-y z{q@(~vVFjS0hv0EO#HCRicwEOt??aS|BzK*-^67`$4+a4Su<-C?rN&9?=*FqOr1^N zgpz_H&5H$OR+9bNI9Odd(GcO~0>l-lC5WSiD-o)Y&yH8mcqf^w<MEUlTiec_Jn>B< znf<CW9`B^~^fVZN4}2Rx&a(ijkJ{Fl6b^^0?f6PcGHOyAX5QIym4s4N<8*d~O(six zUtN;2Fdb>b93$7LOlA7XFF*R|Bd$wFj^M|aC-svpob*~w>Pdb|y4T$1^?v(pM#iL6 zKVmUATFE}EKl*j!nI0+Lspkfczo2^U)n@Oc%*^k;OG|6_4;n~;H~%WRV=X)|zVkSa z&Um8;adpbhHi}WCLRov#TTZnNjICXccvUhf-5ngx);4Qfn>BnUlw)>PiKeESDiIfl zujXca<>VM;Damh1>Q{cp8@E*n6s7dE%P5-A;6jESV?8LXJBX5n3S~igz_+3pbxR7% zp{{URw3;F7iQe9DH)>nAs&@Bwhvcv{Fq;C$Z&Te5xMVfzMA?zzC8JEWV0`(&k6>@u zC}5JQOHH(9_ZPR0o?vIFC)7iNVM>qE(}NWPsYAhJja4?Z?$a5!7<?0Zli^G{>)o%o z&(K^}6xmQeqP@=ABkic?)bED+F<sCzDe73bux$+c#F{5lwk9Vh%i;E>Cd3$`tf{Fz z9A@`WBy&(pPRMjRZA!=o(%DdZcc?og6dRXMB@|Mu4yjG;9grRs!px26=$q>q9T5qZ z8h1h~3ny;T+G-S2a|@k56|R;nD6y`N7DaS-btE9|8D+Ki`_t57?#m278Pl}$;v%)j zY(|mo;85!b%8^RjBRUmLPK{RIUJCb0J-EUAqDLsABivnuVTM1QIKlqZ+~K!$H0vIo zk9c&$pFriB+wHe>qvn2&<5&X|d?FO8sj^W4-n2|K9R+t93hpF|!*2<vFsA?02=A+e zMM~tjNhr3w7UfJnm)_VuFaz>h^7(l!lF^dS&uEccrW<+7_jU6xQdg+8H9o!rdCRXG ziOa!R?fle*q!mg^3I>JWYqoMXk)GPvY1wM*z->Xh*<3Aqkf8jM*(}$%n9_4DOUY(4 zvXsYH4XPdU!Lxo7vQZo|QP^+h@3^Ekr;|Bk%l>NlEYF>-He@6cMov0gEh8U!swJnh z&96sO(l0X7xeTQL=M3a4C^(mV^#7J<tUW#H=`F}Ie*M3t86=ocPLA2^@~60do;D&w z<NA5R;9&E0g<U_-6_G5VM^b|M7dZiMJ6&BW5`sUoKO;1spAwJ^q>c_nxh)dx?Va6< zhCt^`#i`1oDN-1zB^2uNh5d;3&gd+|#1XF=8LP9s9eRXhIUJV!x)IFDaglKTiR1Yv zBcL&O!Jvj{_UlGCw=oIv+`$1&Mnof)rM5Oz?e!yI{U&olYbS@S-^?-F>1#E&v{cKd z5waL%#B9)S<)EcrOn?x#)iOf%bhXsdV)plWJOja@&*RUJ*GQx$pC74_7^Nzng-pXN zWEW!X5zIn%=z)8llRcQ*6l2a&j91xWyvpu>&Y0Wqrpiap$yf7>@L-U2kkdgfebpcj z26@G+a;6nzp0OlFW-Ngn?-+|J&%PtOd4(dDbmSifk{1{o{~=fN!c{DJ+b8|xhdz-f z^9E3v?!ev$wpXy6P>HF~ZcK%4#Z>4nJ+k_`1&Lsm?n08V*O|YP=kS_k-=YVWAOb6X zG+0_duKH1)!^<o@i;J#_phFfcC1{wuo9P0u9E#bMht4e*2AkwFj2Q|{4%E01YGeb` zx17UUGrXLW3pIv9ja<k{X_fEfj>ljE`??G@n0&ZPZsnyJCrrjG98OsDuuyp6$Vv9P zAB56oENg)rhbOt{^hjjC;m~j-tfeFSjbsf8%Q9$hHY?95V3&&aHn2+tyT95DcBvpA zvnl?cldp61lXp6lzwszsjQ(8#^hlsrcn$Jm=vw7fu4jw#F2f{{3t`c4u0htpJY%Th z=N+`j%Dk~)8F-=4AoJ?{2ZhRM?rApA*%aAeJ3PT4X9NAi1m!VawVnX-1dtQa+ZWN> zTQDB)ptn!Jw(r5VD`DGG^tKe3muD#VWMMC$Z`*73T7w;Ld#zim?6JX!zk+1*Rz<xC zYlB|s%)4p4byy6xyyVGiTUlVs`vZv>GG5ah^Ec(@%mgk%NtmQ#Yn#&b!1ZHAwl7_m z%_c4^e3<08#@0k<h)kiMLQb_^b(+);J?T_^Pn#)8PpS`DL-mo?j4&m7nuCdn!JsKv zAJldHZyUu`i}tu`YByofjgRSq5l226Rq)BDrXfd$6y*IgufQb2+4}Kgy<wA-EgkbU zh=yaP-tIV4uw5dP6eqQZ)o{BhtTuPp5+%*l(cF<JbxVo=p-qq0*k*SkS++H{Ii*g? zNuAZg)LBg?6W;xekJ*IRAFLcnQk5n!r?#NBSSUzlGFyyX5jb#C$-x*6(H%HFIGwZ| z#=H^6^ud_3;E}vHbR~>A1jZZ!V`|?h$yn8^^EN6^uy^Rd@}e%UkRA?3oCnQ*2r8Gc zXL8~x$MEOE^~!q;av5Qnxkq`<6_xCcO6E<`Y{UXDA#>!7hh$!;9~_lDJz2e117;v3 zW&$%1+Rg%oSG##}pBGX`Q)G*CATR@=hU8WKyjhtHOfodwnWb_lCWG`smih-~R|E~& zf3Zer>@?j9b`@Y(h0d>s3KdYHkX&eIue&2xUC!&5V}Kb0%ot!UhYEeKaI2xhT3|K- zGX|J3z$A@PH`-!UxVx-hg?(k}opO8vjR(8J2)n?<=>~Z`kjtj4ce1n%AnyeE7D%gs zwA-QK?a**1G`tKQz6a8FK-zCu+QsVUE-bn>u2p%_6#-C}xK1tP1yo*i<t0~MKrN%l z21nm+`(&^&b^w9kMc2*Ot0u;9YAD5%EMTqzW+lewD#HFMI(juQ9KE~=$?Kv|SE?3X zHeCqJL|_(T$X}~5db1GZFKg5i?#)z?hl6|(Sl*1@@IL5v^yXHuyc@RTb=(<XISZKS zyVO?RvSkO&(eZx{P=S+k0aRFk_OsA_0ov#2E}Ztds~%DRzzL66v3cW?SFycMtGo*^ z=4o{x_lPSJzsB?1H}B~7t;zBRIqv}ThC<)Q;vIifzu;GY&PNvj!|TM?9WZ*tIcets zwZa1OERbhG+H8<RuNmaoAg8~s?y*B6?}@TR?4rU)&q?HkYpyPfA<>Ku^G0dI5%oko zoF%$P9r)m=!H#$Ln~tg%a0j@u@CrSrm@VkxRt(8T9sdLYwO4}O2EybS?6}-aZ%|DR z*e@NJ3_Mv1@rz+U4ws7{eo0h(I@)KT{nl31#dUcg+Vi$L5BmmSu7eC-qTqkDxB-|O zpfV3Tmx))p)hSlU;1Y%M1OsCA4DEUEtPna4A}ftX`&6{&nl;FzZPZ}3IJ8$G17$F< zH(ukg<cfDNUYjCC$#GF~Vq!lzKT$iuWFBD)$UMS&vO(s3iqd55D<-Gw4xAoz!WkHr zVvy5yd)@354Q~<&fw`&LKcTV-txUQTYchC&g33Hr>>-KhVgk&_yFn(vrhBzpoY0R4 z{m^i2B&UtVP+%DpScWby1O9U0mjQn{@Qcyq1;8&umzM#PQ>Z<|g?1P)!+;qKt&UDH zwBo?&nxfe)V0i_|i$J~tEKlhc`a#!lHPer}N^gSYez05$mRA5XW}Y_Qi8Q>qLaVjl zs$HwiFhRtksEA<@v2?Q`VhWmX+pPVLgYRaL(;)&AJ){r|CR!TZ3$1rS>&+0c6Cx@h zVjVDLRaz!b4yORa{~|LQEU&xQV96<b=e^o!4)+T|&I36Q<PY{3<O@M=+M|7EgT#_^ zXv6z&f<*qehwIU)n<0_Y>v%}ygeN_tJ;QSb#teu^G*-y`z$^pi7GUlMW<w0-(?4lH z@*K4wiYZ_<UN_VzU^QOX1n=2TijpUQ9D3g%PXamp1FggXi3Pd?(eJ@D$VtD0UFsn5 zF-UwH68Gp<{FXsSp3_kTj%y>WF!?J#Xw%q{D}Y@BY(RGg;?5YX*MRk~GuovNgfj12 zAzA}>OD6Vl-k#bgnSRHi%*B!Yp%A0-9Hc%6sZT)a^N@M~{d^dsQ4HG@1G72KG?_ax z7Z^^T1Hkg2*I>!Xrrv83JQ^7wdqGCD2bu;MWDe|v!KMIo4m_Q2;(yr~ibo&hoBqvp zsc*9Ne2LMTef&s?sfW`*o}s!iv_|3K|F^1vemp(@HT1g^R>_8bdBC(!H(fgvR=H}E JX{kpd`ahOEt{(sZ delta 8602 zcma)B3s_Xu+WyvFd+!+-m;r^EVHk!DiXtL{a+90f1VIo&Fcnkrh6htI&D2yhEltf# ztjSB}?Kdlrc_gLK(=jtsk9n-)EiY-<)jXzMOiyP2yEX%Hdj9ACe|r{tX20wGzIT1= zTi;r1TC&%*|2bDAF(UHEADR5}2UTRRk9+wv(T^Gt+t7b-RvCMVB8YxE@LZ&;?x*YM zd#Gwg)g$Ed%`T$bXjD38QdR9$>+*UcC4k7Fjt9Q%he<n$eg;g}tE;Bf;^_ygc07&M z56>E}{MqLYktLbv*r{stukPP{H2PmBb>+zk<Eq9cPnq}z6gbg8aROc#nhjx8N30w# zI!~B1eP*S-(t!>z!W|D!8B<mMW#}d%9}u`>Qq{~_wuwVFn~DC~<f=*IhLtZkL3G{+ zxKHhrY17wc%u6M@WGAxT*FzxP@jExy@%Bhd+D}G_9{`WOJn;A1`t$6V81bynWqsK4 zhI#B=W|&F(fAs_Tdkr0B9~MR=p$kU~z+Vy60Fu@3p<xxi7-&^}hLUS<?%JH(+_yQq zIk!2lxwQE}^Lx!7G+(%CY*??%VfN-OI-2zd)X>n&ki_b2Va_m{);}yL%um}#6lR1E z3jI3tbIqwGgcgM!)i#Ij3k?nJ5Skv^J9KO4iqOE&64x!)I@hw$d9I0}J6yeqT*qC9 zUAtZHxemCpTs5w7A%6{dSlDrO3VG3a*E!i)?-Y|tPE1XR1<xSoTxW$d$yrwC92h*q z8A2573U=XdXmG6P80;Hta@-D{?HJ&2ID#GTIhdorqt=n+a0KlRnoDF)3F=A|<ZJ)X zUTfQ6`&Dr4ZrcWXwrv$YjTGN)7i_0&U)Yw|2HCgRSKBT2?Y0kX!9aa%`SvP%x%IsD zpSHSc+e%xZ?Tqzp>sPj+wnWU_r?vo_pY<2(ZhW3@b6AhqoYra9Z)|33YT$yvJAuQj zf!6eZuLEudCIqex%nf+fzoXxW{*(NVbzpvDgkygjlJKg(x8<&%-E!En0$KVDnwMpi zWigP?Ew?R3%W(5S^BhY+6ke2D(#%arGcrvzJZxO2PqVP&`NtMvx|gu!^<k!H86QX0 zZ(;#yY#=Lb?IE75kQL#tr?p4$Hbd``rvFW0x%FY*t}ZN6|JvpEuFS>4TfZ?(2EJ?X zWtEr@7$Lq$yS9JVgqr$Y%uv@v`|<Wb8zMgJ^)&GrVf>GO+dSVkw|m<cOQ3)apc)p- zda-ntN-LQ+ZJ>?xDSblc=^~3^DYTZxQUWE>XxdCSn2pZR=X7oqV|0lw&_{HVPEjU( zKu0Nvq7lts=pNmoC^nUvs6Xwb9rQhY$ELAIsW<(~CbGE{M``SFn#uf`g;mima?&>} zm#(of^d9-s1GJqWwjtIhpVfU9r9|aKRY#ra9@+h|?r%r)=w8uFqYw88?lHJWQ;ZrD z5tA4*GUm~koiP`BntR6goYZr9&pka)_xvq3F!q7iS+Ot2nc{NecE)|(E4)`xuT8x! z^<EM0h<`Bt>G=KepCzaXWeLwF97^n(SfBVx;#-MF6VE4pn|LqDH_4UMCuvzyQ(e-j zq@R<`$q~uv$>qt@l3z;xAo*q=bDxwxwS6}CIi8}Vq@>JEIhE>4%}ZU8`a$Y<X)LW{ zT0&ZN+Vr#qX)mPhO8YFmLwaudu=K~$KS;lmk&#iCu{GoEzT^77-}jeHEptd_UFM$5 zOIba#24yYC`Z()uc1U(+c189(IZDp>oHugo5^^8S-I{x$AMaP*Z&|<Vc|mzI^0wxE zmhZ?PoWCG{U;c&u-TRN|f4v~FU|zxN1<i$Cg`EpW7CuvWc|gQ~x&a3V+$hQ{T3Ga# zqGLrjisfSe;vU7B#g)aAi`NwYx%f!&>Edq(4j8z$#J6Nh$)1wtQfuju(x)DXc;L}8 zRyL*V>>$UGL5l{REsrT5T>ftP-HO2#iz>bu96I>r!QVYt_u$zf6Nj7{S~zsx(1Sy7 z4yzgVPNktTzVhkHW5e@?uNi**p_qrZJoM{`nh|e|xIVJ$$k#_Hqe@0?8ufKmw{bO? z-EvF*p!%?}hwD3yJ=k!0oYtkG&+@NJ8aiEE!FVF|sekj*7X5kPQs4SDpT3DoJ@B%f zH`F#48|#~|g*PO8dr)rRH(yiP<ocbrQ`xHeYq#I1dyn4NGx6{AG5yR8%*1?=WB#lo z3uJZ{%tBZw3um1fRw0XG(MWHR`0*@}r6T<^SvJdM`C>`1VphTiv!Se#jbv493>(L) z*`JWTlh_nCRb(@JjLl+?v$+f#nZBg(x88ZDm@R9lx#KT+<+5c0F!(eq`uDR^-Byye z?j~vbBP6{xpQJYrlhou#(z|w&-XBiV(PJbXUr*9UM@TwdNYZ({yYv=GUyUc}dMrsd z4wCfUc9OndLedZ5-Z@UvFSD^X4I?hCC2qhzW!z8Pw2Qd!3gZ46@eUcpt#^nAB@?eh zedk>q#Jim!-s@xHiSvl3MiEa3Irkj#{^N<4zev3D2JunoJ7xp%Y9HbgRuivr5w9IV ze0n+Y87GO)izdEk8}TPs5r1kv@x?X7mkc5PbO!OIk;IpIqunI_3~a7~z~_fk9e?2w z;x8^DzNU%z+V6?4hx3<46Mq@THo@^$nfSJ)#JA5R{@NkpJMn$j1me5Vf6sN|Z-KMF z3-JTF#1Empe~tLDJmMe55dX(0;wQ_9e}wn{#K=z{CVr+64TC+qoA@W;b;K_&A$}R7 zy9%S9uOR-#UgH0fh~EV9CoA#0h{Uh`iQm^q_MJy^$J->^&ynodO|q6qa`<SHyX+;o z8$QP^Cpq3ha^gXfGrlG{>j{!`Mv$C`cll<LOYtlRHd-Zld?Cp-bw@~^GMnT_!bzTS zisV^?NuFOz@<JGT<^;(rZ<D+Zoi|uX-tr>J+pdxP`Y4ij!r5-LH#10XY9{%w$4NeN zpX3i$k^FHv$)~%Je71(<PcD*tVKd2B(nxNGt#9^{{I7c?{}@H`-3=ty-J3uP-%E<& z94V$ZN%1*Cin*2)zg)B{q;!<gZjxff)B0~x9Pwy)I>(}&Bqih;DV?4mC2}AsJy(*F zaE6q=v7}@*k&^owDFYHoDgKg_hoVUtJ&crzH%OTlMamrboBuu4DNA8&Wd<oP_9A8N z22!?0lJd%KQr<XB%I>E~*@yS<;Qi5Uq?|ZT%K7o6T!GPVH<EJeJSo4eCY6sM)i{S# zQ#h&CCrAxmNopqtsS)px+9Q+HIJ`@VBsF6`skzal=5Hglw2ah>OuVdoi_}qFNF578 z^{1pxEhP1^&q<ww&I?{3^~uAeK7Hu<BkU1VCN?zE+W~h!#v*(Vw7tM<_hSs@!{Xk^ z*d|{Kd29aUrP=WebPtqRFwV{1nz?bZ#NJgru*CzX8Qb9DMz%%$1MFjIgQG31!N3CD zSFEkC_S)EIMi46Ct^)QRg1Z}`IvPjjMzh9j-d+~$zy4@}?wBYY@&Pn3iaq1g!$RR; z27?){1yU4wp{Zy_GzHCs#(@Q(nH!BsthWMAFw6y$9W9Wu!N~?E8=PzpCmWm&I2;GV z&xADgk_t`?I5FVFfb)drMXNQH7HdX46?$4T(UTen7Jz1M?3Kgz_&|0DwE1I1H%oNc z{*d5T%C;zgI>1~9jA(y_4s`(QVg)NUbhD5ZkQH7s;iVMH^5CTu%6eclrBD`t|E%a= z_7M9@MSqVeN<#m1sER@V6sYP6Rehk!t_7fvxpDMZHnqL$i+^f!eeX|fL3>k0FjWRq z!gU!;<-t@*J5$?gStlX2)pZH_SG4P&gZ^c|_jjA0XBmyLGudzLoy-Pl8Auc1WCffo z1Zg!$4v*d;b!@J0yHI_)yiM=@<?Jg%JLtyhHVCf9h!l)P6m6KVSOj_AIvtKh5H_r1 zUB%S2#{a}CZOj|5u)FQ!bMwz_kl#-Bw(`G>Hw)5}ejjh+_YEwry}syuZKjI$v2k7y zpta1hyXlY)Iic<RA$Hl|NwqHRQtdeeX&HjF7C~AEPjld@HPya6j^)~JJS)$)dD?ZJ z-PK^u3{OUwGa{!d5c9#L;%TG@Nx^3mo*aDw(9DfzeqsNRndSLyQYWnV7|p!rgw*H@ zBgS?{YG7mvj7-McS{SK?ky;or!iW(@cAKSrA&8b|)lww-cgGkafp$m#aEt+a9`FD( zbK?uqQj(1RHVspz1p@Oz@~e0n@l+tggoNh60?^Ejw__!)qKy1IQ4)1YfeA;F^pgp* zF3MI%t)*-^b97jgt+X8JzkC6QU=*={dtp)DDAnN*u+0xhcl|I(!47F*qi%Rn8boAj zMhIxBCA}fS8zNxRoi{=1-B>+Z+ARvo|3ysyo!AJXmq^fbI7@=FbZ8kqS=W*dsdFYv z+#9gPGZG)b%V+Aa1@MlUlF1t$6^z6Lwt`9Bi1@CB)Q=%`38bcICX^@+tR*;~KOsF} z{sU(QI5WVR3r-C<BfuHajx+i>>3QL&-9A9Q5R$Rrlz}6vLN78=LA#N4UN3zjCczGE z5iMbKdvDjJ+Y#lc?b0X4ut21N7gfT{P?#ADGX-$nK0tfllJX7iiEl|!|G&;t4d|DL z@zlcTXc(R48Bb-q@$^3+_4Y!}%sHy-5lONBsC2b;v-0=IU4F6+-f>duBw#TS0f5Cs zTs+e@5%<nWabhQI52svggNI#|5_C3V5rBvY_WI@)7_r#jEKM|e!t2-KThF*63_%p# zJPe^5!rMMB<=;v{!lVk5zTl`7=$?H`H>twp+FMdqt6s4+Ti|id{Wd}C?@Q^byWf2& z$|L%J?o%yHv`c5VpTdKgxiMPdgT!$C5e?50aWD7gV*QD2ZHG7iOY6CyHI_AgI`b(% z9w*AaFzSJ*Gg6=cLrua^U8LZ<2~8|6bK?~&|4oJ`Gy03AY{odQ2kV~9fPI{Nl2~#A z7Sp1kV3c9<MMhP^Tr$j!(M))9^1@oxz}XhY$0&-0{9wx5RX62_*D+mr7>6eVwU{y> zpu?kd*g#l`QT%ZOJhg)<2cELwsR*9ZFh>u-QyS9D2v363xffq7P76YvSe`gO>HG3f z&tNOSiKLdz!T?SO$T*kGhqgxD+`@5Z^wnjEc#P=F3q%F>f=Uq&FA79jc+o(ZO!vr0 zgp5eY5Cistj8jGYvX~AbBM2N3!vke)F}za7D-1a6B@W}En840p3>(J#i3%y!ZYMD8 zh{cX-ogvol@oIk70N4RI9IylMt|>a~0DN)^KP{?7GGH+&$<VkO8lQy5)zByoNjos? z#n6~c0chsNk7x7V7I4IICkz~M<PqCb4LG90)Pge(;~D~v730d#aIjj+^MxO=dj~=z zPEhFA;-?mzW#G&MrxqM>fQy2kZL9fNle^zG?t=byN$9TK))wQH+xVtGrtg2kk1Osq zO<b&<6!=bo?-YdKWRq?}993^N@i7*_5r8$oPQas%>2L(#`Nw#usEwj-cfe>-x7|BV z>kH(_X>Jp-Z4X_)s58aZ=5vWJ5J%+p@Y*lh;HEElmZ&7znm<LsaEum6GZ7MTKAne< zhz;RIgk(BGQh+e#Y39br8+?EnCR?*qq{sYwx=Am<8}IP}941AuT9a7hT^vkCz+`ur z>;{u|YTFL?*yUS-(hZa@P|*V_!a<1#MJ)1oP|`pVN!kr6BH<=cmKTe0x8t~vnPgE? z1bodT{~(qH3XGnS0f^YO4mympN~3p2`6qEC^9JmVG!Tc3IBGfl#vxc5neZI~VZa;x zBZK5uMOg_#e;+6eg1JvabaO#~?}x|>{|K)LZ-aM+%kPUxD98AQgB=I<{O&qj0j(Rm z%Tq-_#i6b*-~<HJm7>EMjAx|C3w;2$9B^7TgA;i=EY34G^5pNtNW;MnC;ckpzE-Tm z;b40YlwTG}p9VM$updUd8>5|z*d9V`w;;A3Ahu%>+f0l$6Pz<c<Yi%AZFdQG&1~JA zxHc@EEwAxGj30;R$i=c!f*?AeS*#^-<w${9u?6M8Y#7Xn9o`R9CQcuN&&abv-FI)x zVj*<Z{L$=af$rF!b^Be>r}SsJBZ^Do*!%JpaY572P3#z=P=~|5*to)BKhk4gWRV8@ zqEN@1l%Zl4#2PC_&q#!Mtwj+jkxk~tCX4cFpvSY#<GD6MhlS^r5z4b7j-B8!6mTa9 zi;mXePJoM|m4)Kc(h+b+h{=VRz8GvS#1ue`xGfIAU^_xgNA$nbTX|iWOM)0NEhuX4 zT`9V`B#1egqKp(3GZAni;6%V_{dG7I@bLc16rXn9!r;w`p@+enlN1U<Xq^~(tG6G^ zmBnHOrNUf7i@CPaRs&8Q6gGe}2ntg%%c=0OeTcGHNSFl93UKBM3FCDM&j<<Ql}`di znV+MGG9QRn!{#UnV$F#%FN(aV<Lejd3{mFyFH{Ty7I~ZrI1KQ~r8+E1>CL5zm&n92 zz!`wcU{jP|QL<-2;}mFI0F9HNu>=~+!0Gb5k}fpnf+OzFB`{gLSvOe#c;#m02XP-0 z$<#%|QTGiU>Z0LH@rLrc5fKy{pSZt>jZYl&s^Co|sMt%z*)a~@#HKg@Eu~Xy9Tf)+ zQDS%fO*bd%=*hn+7sSCq>^I#3x9&H>+z6Pfgt=0ftAM!#q<~l}=iXPUyuc{{rvMTP zA>nmMcnO@%;A{rR4UQX}0t~niobmrq)`*Qj+?a$6T)W&pmvtH9auj`8x$28ic`l$d z0NRU4#nXtVg1K*5a$@@uxAx?JDc!}WL>Uy@zbJ#XKkDX08C?0JV(>@&oPb?`HY%!E zEiOO@6g5!n44nXpn=mF5GvkG8n@ZIXKLg^YLHsm`*I-vvD#@(om|#v+Nl_sO2I=NR zg`6CuP7%vN6kG-7P;}kbLUmZwS8q+tkvy=s2ObvQ2G0&xuL@X9rl<j!Om|I$4vV>4 z8lhg2pw+4QquHT15V`4vrlJ|q6udLx$$<x;nHyI}s*EGOU+AuW1jz<a4H|+Ity?vq z$I57RZUCYorm{np8YkpeK)?tH7y$vL`Fht8fNS&BSHvAD3UCzQ2v4$=XexgBFrq0m z0l^Xn@+bsLEQj2|s>KT?hl4W+oZ&DzYNT#*IN<ps)zknK!u`|KdM`Xn=c@SzxESPd z5d{}ZF~;Q><5G-qImS2(W1I;Wg>aDr8;2ID>%{7r1&$Lg#Hr%pRl18Bz{^*uq2dgc z16cf^)f*;@HQelAvJk2#LiIG5d=MscVNw*)bDPxOViETMCmNh+Ap;zd)AitpGt?$< z#IFoJ!07=_@(%SYv4N%k?#CVdwk|`|p`y3dSHzuFz#%O%UO3c-YKPQsO>h|RLF3_2 zTv?aE;j3^c_OBywxDgKH;V@n}JgG8qBk$|sq=B;xoIT*Y1kStQ9MxJDUK%)k!5M!= zP4|b2c+DRdd^>78*8eSCMwEv2e@l%sVH%`g)qW~WSE1VwbaQKgxIO=VR(%qt#hMmB zMUB=1XaY=6=Y|LUv1rBd7bj!vO)ZPIlv;k8D}j9cZvtQe=wojDF~G1+ELyRTiEK*1 zw5@j<#L7$oyx(ca@&jB1Sk%N2z}`J{SX7Xx9tI|6$OYH|80#9lbxWC@iNQ~T!l_VL z4TUav6pMXGlEID9xo>70#4k79@XDTJI4yP{QSd~utAq)0OA?1`QS6E!dJIgo{+?(p kHT){hIML`Y_68(|TN|pYiiWw&p@xaw5WgKy7>3#Z8!J^;m;e9( diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt index d9bb7ffb45..595b7cf845 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt @@ -30,6 +30,7 @@ object MaterialSymbols { val AddCircle = MaterialSymbol("\uE3BA") val AddPhotoAlternate = MaterialSymbol("\uE43E") val AddReaction = MaterialSymbol("\uE1D3") + val AddToHomeScreen = MaterialSymbol("\uE1FE") val AlternateEmail = MaterialSymbol("\uE0E6") val AltRoute = MaterialSymbol("\uF184") val Apps = MaterialSymbol("\uE5C3") @@ -88,6 +89,7 @@ object MaterialSymbols { val DeleteForever = MaterialSymbol("\uE92B") val DeleteSweep = MaterialSymbol("\uE16C") val Description = MaterialSymbol("\uE873") + val DesktopWindows = MaterialSymbol("\uE30C") val DirectionsBike = MaterialSymbol("\uE52F") val DirectionsRun = MaterialSymbol("\uE566") val DirectionsWalk = MaterialSymbol("\uE536") @@ -126,6 +128,7 @@ object MaterialSymbols { val FormatItalic = MaterialSymbol("\uE23F") val FormatListNumbered = MaterialSymbol("\uE242") val FormatQuote = MaterialSymbol("\uE244") + val FormatSize = MaterialSymbol("\uE245") val FormatStrikethrough = MaterialSymbol("\uE246") val Forum = MaterialSymbol("\uE8AF") val Forward = MaterialSymbol("\uF57A") From 2111e18e529049ca05de18727173f651b385bea9 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 16:15:39 +0000 Subject: [PATCH 06/60] feat(browser): Chrome-PWA parity in the :napplet browser surfaces - NappletControlSheet rebuilt on BrowserChrome: title + origin/security header (tap: page info, long-press: copy), icon row (back, forward, reload/stop, star, share), menu rows incl. rarely-used Edit address, find in page, text size, desktop site, add to Home screen, open in another browser, Privacy and Developer groups; same glyphs as Compose. - Full-screen browser: new windows for _blank/window.open (opener kept via BrowserPopups), intent: URIs, downloads (Tor-aware, MediaStore, blob/data via page script), HTML fullscreen, camera/mic/location prompts, theme-color system bars, Recents task description, long-press link/image menu, find bar, navigator.share polyfill, crash recovery. - Embedded browser service: IPC for all of the above; JS dialogs and permission prompts relayed to the main process; fullscreen inside the surface; WebView wrapped in a container so it can be rebuilt. - onRenderProcessGone handled in all four :napplet WebView owners. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TEkj7Eo2xbidVHAoF8GZKQ --- .../napplethost/BrowserChromeLabels.kt | 157 +++ .../amethyst/napplethost/BrowserDownloads.kt | 207 ++++ .../napplethost/BrowserExtrasScript.kt | 175 ++++ .../amethyst/napplethost/BrowserFindBar.kt | 197 ++++ .../amethyst/napplethost/BrowserJsDialogs.kt | 13 +- .../amethyst/napplethost/BrowserPopups.kt | 137 +++ .../amethyst/napplethost/BrowserWebTools.kt | 376 +++++++ .../napplethost/NappletBrowserActivity.kt | 983 ++++++++++++++---- .../napplethost/NappletBrowserContract.kt | 85 ++ .../napplethost/NappletBrowserService.kt | 464 ++++++++- .../napplethost/NappletBrowserUiAdapter.kt | 21 +- .../napplethost/NappletControlSheet.kt | 835 ++++++++------- .../napplethost/NappletHostActivity.kt | 101 +- .../napplethost/NappletHostService.kt | 44 +- .../napplethost/NappletHostUiAdapter.kt | 21 +- .../amethyst/napplethost/NappletIpc.kt | 32 + nappletHost/src/main/res/values/strings.xml | 7 - 17 files changed, 3200 insertions(+), 655 deletions(-) create mode 100644 nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserChromeLabels.kt create mode 100644 nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserDownloads.kt create mode 100644 nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserExtrasScript.kt create mode 100644 nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserFindBar.kt create mode 100644 nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserPopups.kt create mode 100644 nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserWebTools.kt diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserChromeLabels.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserChromeLabels.kt new file mode 100644 index 0000000000..088aac02a3 --- /dev/null +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserChromeLabels.kt @@ -0,0 +1,157 @@ +/* + * 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.napplethost + +import android.content.Context +import android.graphics.Typeface +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.Action +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.Security +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.napplet.NappletWebContract +import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.amethyst.commons.R as CommonsR + +/** + * How each [BrowserChrome.Action] looks: its Material Symbol (or, for Tor, the onion drawable) and its + * label. Shared by both renderers of the top pill — the Compose sheet over embedded tabs and the + * plain-View sheet in the full-screen browser — so an action has one icon and one name everywhere. + */ +object BrowserChromeLabels { + /** The glyph for [action], or null when it is drawn from [drawableFor] instead. */ + fun symbolFor(action: Action): MaterialSymbol? = + when (action) { + Action.BACK -> MaterialSymbols.AutoMirrored.ArrowBack + Action.FORWARD -> MaterialSymbols.AutoMirrored.ArrowForward + Action.RELOAD -> MaterialSymbols.Refresh + Action.STOP -> MaterialSymbols.Close + Action.FAVORITE -> MaterialSymbols.Star + Action.SHARE -> MaterialSymbols.Share + Action.BACK_TO_APP -> MaterialSymbols.Home + Action.COPY_LINK -> MaterialSymbols.ContentCopy + Action.EDIT_ADDRESS -> MaterialSymbols.Edit + Action.FIND_IN_PAGE -> MaterialSymbols.Search + Action.TEXT_SIZE -> MaterialSymbols.FormatSize + Action.DESKTOP_SITE -> MaterialSymbols.DesktopWindows + Action.ADD_TO_HOME_SCREEN -> MaterialSymbols.AddToHomeScreen + Action.OPEN_IN_BROWSER_APP -> MaterialSymbols.OpenInBrowser + Action.OPEN_FULL_SCREEN -> MaterialSymbols.OpenInFull + Action.TOR -> null + Action.ACCESS_INFO -> MaterialSymbols.Info + Action.SITE_SETTINGS -> MaterialSymbols.Tune + Action.CONSOLE -> MaterialSymbols.Code + } + + @DrawableRes + fun drawableFor(action: Action): Int? = if (action == Action.TOR) R.drawable.ic_tor else null + + /** The label for [action]; [isFavorite] and [torOn] pick the stateful wording. */ + @StringRes + fun labelFor( + action: Action, + isFavorite: Boolean = false, + torOn: Boolean = false, + ): Int = + when (action) { + Action.BACK -> CommonsR.string.browser_action_back + Action.FORWARD -> CommonsR.string.browser_action_forward + Action.RELOAD -> CommonsR.string.browser_action_reload + Action.STOP -> CommonsR.string.browser_action_stop + Action.FAVORITE -> if (isFavorite) CommonsR.string.browser_action_favorite_remove else CommonsR.string.browser_action_favorite_add + Action.SHARE -> CommonsR.string.browser_action_share + Action.BACK_TO_APP -> CommonsR.string.browser_action_back_to_app + Action.COPY_LINK -> CommonsR.string.browser_action_copy_link + Action.EDIT_ADDRESS -> CommonsR.string.browser_action_edit_address + Action.FIND_IN_PAGE -> CommonsR.string.browser_action_find_in_page + Action.TEXT_SIZE -> CommonsR.string.browser_action_text_size + Action.DESKTOP_SITE -> CommonsR.string.browser_action_desktop_site + Action.ADD_TO_HOME_SCREEN -> CommonsR.string.browser_action_add_to_home + Action.OPEN_IN_BROWSER_APP -> CommonsR.string.browser_action_open_in_browser_app + Action.OPEN_FULL_SCREEN -> CommonsR.string.browser_action_open_full_screen + Action.TOR -> if (torOn) CommonsR.string.browser_action_tor_on else CommonsR.string.browser_action_tor_off + Action.ACCESS_INFO -> CommonsR.string.browser_action_access_info + Action.SITE_SETTINGS -> CommonsR.string.browser_action_site_settings + Action.CONSOLE -> CommonsR.string.browser_console_title_short + } + + /** Actions whose row carries an on/off switch. */ + fun isToggle(action: Action): Boolean = action == Action.TOR || action == Action.DESKTOP_SITE || action == Action.CONSOLE + + /** Actions that keep the sheet open when used (the text-size stepper, the address editor). */ + fun keepsSheetOpen(action: Action): Boolean = action == Action.TEXT_SIZE || action == Action.EDIT_ADDRESS + + fun securitySymbol(security: Security): MaterialSymbol? = + when (security) { + Security.TOR -> null + Security.HTTPS -> MaterialSymbols.Lock + Security.HTTP -> MaterialSymbols.NoEncryption + Security.SANDBOX -> MaterialSymbols.Shield + } + + @DrawableRes + fun securityDrawable(security: Security): Int? = if (security == Security.TOR) R.drawable.ic_tor else null + + @StringRes + fun securityLabel(security: Security): Int = + when (security) { + Security.TOR -> CommonsR.string.browser_security_tor + Security.HTTPS -> CommonsR.string.browser_security_https + Security.HTTP -> CommonsR.string.browser_security_http + Security.SANDBOX -> CommonsR.string.browser_security_sandbox + } +} + +/** + * The Material Symbols font the Compose UI draws its icons from, loaded for plain Views. It ships inside + * `:commonsUI`'s compose resources (the same APK assets the napplet shim is read from), so the full-screen + * browser's controls use the very same glyphs as the embedded tab's. + */ +object BrowserGlyphs { + private const val FONT_PATH = "font/material_symbols_outlined.ttf" + + @Volatile private var cached: Typeface? = null + + @Volatile private var cachedFilled: Typeface? = null + + /** The same font with its FILL axis at 1 — a pinned page's solid star, as the Compose icon draws it. */ + fun filledTypeface(context: Context): Typeface = + cachedFilled ?: synchronized(this) { + cachedFilled ?: ( + runCatching { + Typeface + .Builder(context.assets, NappletWebContract.RESOURCE_ASSET_ROOT + FONT_PATH) + .setFontVariationSettings("'FILL' 1") + .build() + }.getOrNull() ?: typeface(context) + ).also { cachedFilled = it } + } + + fun typeface(context: Context): Typeface = + cached ?: synchronized(this) { + cached ?: runCatching { Typeface.createFromAsset(context.assets, NappletWebContract.RESOURCE_ASSET_ROOT + FONT_PATH) } + .onFailure { Log.w("BrowserGlyphs", "Material Symbols font missing from assets", it) } + .getOrDefault(Typeface.DEFAULT) + .also { cached = it } + } +} diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserDownloads.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserDownloads.kt new file mode 100644 index 0000000000..6e97344b45 --- /dev/null +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserDownloads.kt @@ -0,0 +1,207 @@ +/* + * 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.napplethost + +import android.content.ContentValues +import android.content.Context +import android.os.Build +import android.os.Environment +import android.os.Handler +import android.os.Looper +import android.provider.MediaStore +import android.util.Base64 +import android.webkit.MimeTypeMap +import android.webkit.URLUtil +import android.widget.Toast +import com.vitorpamplona.quartz.utils.Log +import okhttp3.Request +import java.io.File +import java.io.OutputStream +import java.net.URLDecoder +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import com.vitorpamplona.amethyst.commons.R as CommonsR + +/** + * Saves what a page downloads — `<a download>`, `Content-Disposition: attachment`, `data:` URLs, and + * `blob:` URLs (which only the page can read, so the browser-extras script hands their bytes over) — into + * the system Downloads collection, the way Chrome does. + * + * Network downloads follow the page's own route: through the Tor SOCKS proxy when the site is on Tor (OkHttp + * leaves SOCKS hosts unresolved, so even DNS goes through Tor), directly otherwise. They carry the page's + * cookies from its own per-account storage profile and its user agent, so a logged-in download works. + */ +object BrowserDownloads { + private const val TAG = "BrowserDownloads" + + /** Cap for bytes a page hands over for a blob:/data: download (they travel as base64 over the bridge). */ + const val MAX_INLINE_BYTES = 25 * 1024 * 1024 + + private val io = Executors.newSingleThreadExecutor { Thread(it, "napplet-downloads").apply { isDaemon = true } } + private val main = Handler(Looper.getMainLooper()) + + /** + * A WebView `DownloadListener` hit. [cookie] must be read on the main thread (from the tab's own + * profile) before calling; the transfer itself runs on a background thread. + */ + fun download( + context: Context, + url: String, + userAgent: String?, + contentDisposition: String?, + mimeType: String?, + cookie: String?, + proxyPort: Int, + ) { + val app = context.applicationContext + if (url.startsWith("data:", ignoreCase = true)) { + saveDataUrl(app, url, null) + return + } + if (!url.startsWith("https://", ignoreCase = true) && !url.startsWith("http://", ignoreCase = true)) return + val name = URLUtil.guessFileName(url, contentDisposition, mimeType) + toast(app, app.getString(CommonsR.string.browser_download_started, name)) + io.execute { + val ok = + runCatching { + val request = + Request + .Builder() + .url(url) + .apply { + userAgent?.takeIf { it.isNotBlank() }?.let { header("User-Agent", it) } + cookie?.takeIf { it.isNotBlank() }?.let { header("Cookie", it) } + }.get() + .build() + // No end-to-end call timeout: a large file over Tor legitimately takes minutes. The + // client's read timeout still ends a transfer that stalls completely. + val client = + NappletBlobHttp + .client(proxyPort) + .newBuilder() + .callTimeout(0, TimeUnit.SECONDS) + .build() + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) error("HTTP ${response.code}") + val type = mimeType?.takeIf { it.isNotBlank() && it != "application/octet-stream" } ?: response.body.contentType()?.let { "${it.type}/${it.subtype}" } + write(app, name, type) { out -> response.body.byteStream().use { it.copyTo(out) } } + } + }.onFailure { Log.w(TAG, "Download failed for $url", it) } + .getOrDefault(false) + toast(app, app.getString(if (ok) CommonsR.string.browser_download_saved else CommonsR.string.browser_download_failed, name)) + } + } + + /** Saves a `data:` URL (`data:[mime][;base64],payload`). */ + fun saveDataUrl( + context: Context, + dataUrl: String, + suggestedName: String?, + ) { + val app = context.applicationContext + val header = dataUrl.substringBefore(',', "") + val payload = dataUrl.substringAfter(',', "") + val mime = header.removePrefix("data:").substringBefore(';').ifBlank { "application/octet-stream" } + val bytes = + runCatching { + if (header.endsWith(";base64", ignoreCase = true)) { + Base64.decode(payload, Base64.DEFAULT) + } else { + URLDecoder.decode(payload, "UTF-8").toByteArray() + } + }.getOrNull() ?: return + saveBytes(app, suggestedName, mime, bytes) + } + + /** Saves bytes a page handed over (a `blob:` download, via the browser-extras script). */ + fun saveBytes( + context: Context, + suggestedName: String?, + mimeType: String?, + bytes: ByteArray, + ) { + if (bytes.size > MAX_INLINE_BYTES) return + val app = context.applicationContext + val name = safeName(suggestedName, mimeType) + io.execute { + val ok = runCatching { write(app, name, mimeType) { it.write(bytes) } }.getOrDefault(false) + toast(app, app.getString(if (ok) CommonsR.string.browser_download_saved else CommonsR.string.browser_download_failed, name)) + } + } + + /** + * Writes into the public Downloads collection (Android 10+, no permission needed), or into the app's + * own Downloads folder on older versions, where writing the shared one would need a storage permission + * the app doesn't hold. + */ + private fun write( + context: Context, + name: String, + mimeType: String?, + body: (OutputStream) -> Unit, + ): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val resolver = context.contentResolver + val values = + ContentValues().apply { + put(MediaStore.Downloads.DISPLAY_NAME, name) + mimeType?.let { put(MediaStore.Downloads.MIME_TYPE, it) } + put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + put(MediaStore.Downloads.IS_PENDING, 1) + } + val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values) ?: return false + return try { + resolver.openOutputStream(uri)?.use(body) ?: error("No output stream") + resolver.update(uri, ContentValues().apply { put(MediaStore.Downloads.IS_PENDING, 0) }, null, null) + true + } catch (e: Exception) { + resolver.delete(uri, null, null) + throw e + } + } + val dir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) ?: return false + dir.mkdirs() + File(dir, name).outputStream().use(body) + return true + } + + /** A plain filename: the page's suggestion without path parts, else "download" + the MIME's extension. */ + private fun safeName( + suggested: String?, + mimeType: String?, + ): String { + val base = + suggested + ?.substringAfterLast('/') + ?.substringAfterLast('\\') + ?.replace(Regex("[\\u0000-\\u001f:*?\"<>|]"), "_") + ?.trim() + ?.takeIf { it.isNotEmpty() && it != "." && it != ".." } + if (base != null) return base.take(120) + val ext = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } + return if (ext != null) "download.$ext" else "download" + } + + private fun toast( + context: Context, + text: String, + ) = main.post { Toast.makeText(context, text, Toast.LENGTH_SHORT).show() } +} diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserExtrasScript.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserExtrasScript.kt new file mode 100644 index 0000000000..acb3b06ec1 --- /dev/null +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserExtrasScript.kt @@ -0,0 +1,175 @@ +/* + * 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.napplethost + +/** + * A document-start script injected only into the **browser** surfaces (never into sandboxed napplets or + * nsites), filling in what an installed Chrome PWA gets and a WebView doesn't: + * + * - `navigator.share` / `navigator.canShare` — WebView has no Web Share. Text, title and url go to the + * Android share sheet (`browser.share`); files are declined, as `canShare` reports. + * - `<meta name="theme-color">` — reported (`browser.themeColor`, normalized to `rgb(r, g, b)`) whenever it + * or the colour scheme changes, so the window can tint its system bars and Recents entry. + * - `blob:` / `data:` downloads — only the page can read a `blob:` URL, so a click on (or a programmatic + * `.click()` of) an `<a download>` pointing at one is turned into its bytes (`browser.download`). + * + * Messages travel over the same origin-scoped native bridge as NIP-07; the host handles `browser.*` types + * itself and never forwards them to the broker. + */ +object BrowserExtrasScript { + val JS: String = + """ + (function () { + if (window.top !== window || window.__amethystBrowserExtras) return; + window.__amethystBrowserExtras = true; + var MAX_BYTES = ${BrowserDownloads.MAX_INLINE_BYTES}; + + function send(message) { + try { var b = window.__nappletBridge; if (b) b.postMessage(JSON.stringify(message)); } catch (_) {} + } + + // ---- Web Share ---- + if (!navigator.share) { + var hasFiles = function (data) { return !!(data && data.files && data.files.length); }; + var shareUrl = function (data) { + if (!data || data.url === undefined || data.url === null) return ''; + try { return new URL(String(data.url), document.baseURI).href; } catch (_) { return null; } + }; + navigator.share = function (data) { + data = data || {}; + var activation = navigator.userActivation; + if (activation && !activation.isActive) { + return Promise.reject(new DOMException('Must be handling a user gesture to perform a share request.', 'NotAllowedError')); + } + if (hasFiles(data)) return Promise.reject(new DOMException('Sharing files is not supported.', 'NotAllowedError')); + var url = shareUrl(data); + if (url === null) return Promise.reject(new TypeError('Invalid URL')); + if (!data.title && !data.text && !url) return Promise.reject(new TypeError('No data to share.')); + send({ type: 'browser.share', title: data.title ? String(data.title) : '', text: data.text ? String(data.text) : '', url: url }); + return Promise.resolve(); + }; + navigator.canShare = function (data) { + if (!data || hasFiles(data) || shareUrl(data) === null) return false; + return !!(data.title || data.text || data.url); + }; + } + + // ---- theme-color ---- + var lastRaw; + var lastColor; + function pickThemeColor() { + var metas = document.querySelectorAll('meta[name="theme-color"]'); + for (var i = 0; i < metas.length; i++) { + var media = metas[i].getAttribute('media'); + try { if (!media || window.matchMedia(media).matches) return metas[i].getAttribute('content'); } catch (_) {} + } + return null; + } + function normalize(color) { + if (!color) return ''; + var root = document.body || document.documentElement; + if (!root) return ''; + var probe = document.createElement('span'); + probe.style.display = 'none'; + probe.style.color = color; + if (!probe.style.color) return ''; + root.appendChild(probe); + var computed = getComputedStyle(probe).color; + probe.remove(); + return computed || ''; + } + function reportTheme() { + var raw = pickThemeColor(); + // Only a changed declaration is worth a style computation. + if (raw === lastRaw && lastColor !== undefined) return; + lastRaw = raw; + var color = normalize(raw); + if (color === lastColor) return; + lastColor = color; + send({ type: 'browser.themeColor', color: color }); + } + var themeTimer = 0; + function scheduleTheme() { clearTimeout(themeTimer); themeTimer = setTimeout(reportTheme, 50); } + function watchTheme() { + reportTheme(); + // <meta> lives in <head>: watching only there keeps busy pages (feeds re-rendering the body) + // from waking this up on every DOM change. + try { + if (document.head) { + new MutationObserver(scheduleTheme).observe(document.head, { + subtree: true, childList: true, attributes: true, attributeFilter: ['content', 'media', 'name'] + }); + } + } catch (_) {} + try { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () { lastRaw = undefined; scheduleTheme(); }); + } catch (_) {} + } + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', watchTheme, { once: true }); + else watchTheme(); + + // ---- blob: / data: downloads ---- + // Pages often revoke a blob URL right after clicking it, before an async fetch could read it, so + // keep a handle on each Blob until a minute after its URL is revoked (the page holds it until the + // revoke anyway, so this doesn't change its lifetime by more than that minute). + var blobs = new Map(); + try { + var nativeCreate = URL.createObjectURL; + var nativeRevoke = URL.revokeObjectURL; + URL.createObjectURL = function (obj) { + var url = nativeCreate.apply(URL, arguments); + try { if (obj instanceof Blob) blobs.set(url, obj); } catch (_) {} + return url; + }; + URL.revokeObjectURL = function (url) { + setTimeout(function () { blobs.delete(url); }, 60000); + return nativeRevoke.apply(URL, arguments); + }; + } catch (_) {} + function isInlineDownload(a) { + return !!(a && a.hasAttribute && a.hasAttribute('download') && /^(blob|data):/i.test(a.href || '')); + } + function deliver(a) { + var name = a.getAttribute('download') || ''; + var known = blobs.get(a.href); + (known ? Promise.resolve(known) : fetch(a.href).then(function (r) { return r.blob(); })).then(function (blob) { + if (blob.size > MAX_BYTES) return; + var reader = new FileReader(); + reader.onload = function () { send({ type: 'browser.download', name: name, mime: blob.type || '', data: String(reader.result) }); }; + reader.readAsDataURL(blob); + }).catch(function () {}); + } + document.addEventListener('click', function (e) { + var a = e.target && e.target.closest ? e.target.closest('a[download]') : null; + if (!isInlineDownload(a)) return; + e.preventDefault(); + deliver(a); + }, true); + // Libraries usually build a detached <a download href="blob:…"> and call .click() on it; a click + // on a detached element never reaches the document listener above. + var nativeClick = HTMLAnchorElement.prototype.click; + HTMLAnchorElement.prototype.click = function () { + if (!this.isConnected && isInlineDownload(this)) { deliver(this); return; } + return nativeClick.apply(this, arguments); + }; + })(); + """.trimIndent() +} diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserFindBar.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserFindBar.kt new file mode 100644 index 0000000000..d96a24e118 --- /dev/null +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserFindBar.kt @@ -0,0 +1,197 @@ +/* + * 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.napplethost + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.text.Editable +import android.text.InputType +import android.text.TextWatcher +import android.util.TypedValue +import android.view.Gravity +import android.view.View +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.webkit.WebView +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.content.ContextCompat +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.R as CommonsR + +/** + * The find-in-page bar for the full-screen surfaces, docked at the bottom edge (the top edge belongs to the + * pill's grabber, and the bottom keeps it off the page's own header). Drives [WebView.findAllAsync] / + * [WebView.findNext] on [webView] and shows "n/m". Hidden until [show]. + */ +@SuppressLint("ViewConstructor") +class BrowserFindBar( + context: Context, + private val webViewProvider: () -> WebView?, + private val onClosed: () -> Unit = {}, +) : LinearLayout(context) { + private val onSurface = color(android.R.attr.textColorPrimary) + private val dimmed = color(android.R.attr.textColorSecondary) + private val glyphs = BrowserGlyphs.typeface(context) + + private val field: EditText + private val count: TextView + + init { + orientation = HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + visibility = View.GONE + elevation = dp(8).toFloat() + background = + GradientDrawable().apply { + cornerRadii = floatArrayOf(dp(16).toFloat(), dp(16).toFloat(), dp(16).toFloat(), dp(16).toFloat(), 0f, 0f, 0f, 0f) + setColor(color(android.R.attr.colorBackground)) + } + setPadding(dp(12), dp(4), dp(4), dp(4)) + field = + EditText(context).apply { + hint = context.getString(CommonsR.string.browser_find_hint) + setTextColor(onSurface) + setHintTextColor(dimmed) + background = null + isSingleLine = true + textSize = 15f + inputType = InputType.TYPE_CLASS_TEXT + imeOptions = EditorInfo.IME_ACTION_SEARCH + layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f) + addTextChangedListener( + object : TextWatcher { + override fun beforeTextChanged( + s: CharSequence?, + start: Int, + count: Int, + after: Int, + ) = Unit + + override fun onTextChanged( + s: CharSequence?, + start: Int, + before: Int, + count: Int, + ) = Unit + + override fun afterTextChanged(s: Editable?) = search(s?.toString().orEmpty()) + }, + ) + setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + webViewProvider()?.findNext(true) + true + } else { + false + } + } + } + count = + TextView(context).apply { + setTextColor(dimmed) + textSize = 13f + setPadding(dp(8), 0, dp(4), 0) + } + addView(field) + addView(count) + addView(button(MaterialSymbols.KeyboardArrowUp, CommonsR.string.browser_find_previous) { webViewProvider()?.findNext(false) }) + addView(button(MaterialSymbols.KeyboardArrowDown, CommonsR.string.browser_find_next) { webViewProvider()?.findNext(true) }) + addView(button(MaterialSymbols.Close, CommonsR.string.browser_find_close) { hide() }) + } + + val isShowing: Boolean get() = visibility == View.VISIBLE + + fun show() { + webViewProvider()?.setFindListener { active, total, _ -> + count.text = + if (total > 0) { + context.getString(CommonsR.string.browser_find_count, active + 1, total) + } else if (field.text.isNullOrEmpty()) { + "" + } else { + "0/0" + } + } + visibility = View.VISIBLE + field.requestFocus() + field.selectAll() + context.getSystemService(InputMethodManager::class.java)?.showSoftInput(field, 0) + } + + fun hide() { + if (!isShowing) return + visibility = View.GONE + context.getSystemService(InputMethodManager::class.java)?.hideSoftInputFromWindow(field.windowToken, 0) + webViewProvider()?.apply { + clearMatches() + setFindListener(null) + } + count.text = "" + onClosed() + } + + private fun search(query: String) { + val wv = webViewProvider() ?: return + if (query.isEmpty()) { + wv.clearMatches() + count.text = "" + } else { + wv.findAllAsync(query) + } + } + + private fun button( + symbol: MaterialSymbol, + label: Int, + onClick: () -> Unit, + ): View = + TextView(context).apply { + text = symbol.glyph + typeface = glyphs + setTextColor(onSurface) + setTextSize(TypedValue.COMPLEX_UNIT_DIP, 22f) + gravity = Gravity.CENTER + minWidth = dp(44) + minHeight = dp(44) + contentDescription = context.getString(label) + tooltipText = contentDescription + isClickable = true + background = + TypedValue().let { tv -> + context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, tv, true) + ContextCompat.getDrawable(context, tv.resourceId) + } + setOnClickListener { onClick() } + } + + private fun color(attr: Int): Int { + val tv = TypedValue() + context.theme.resolveAttribute(attr, tv, true) + return if (tv.resourceId != 0) ContextCompat.getColor(context, tv.resourceId) else tv.data.takeIf { it != 0 } ?: Color.GRAY + } + + private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt() +} diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserJsDialogs.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserJsDialogs.kt index 69ded7e3e3..d2df5f3947 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserJsDialogs.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserJsDialogs.kt @@ -28,6 +28,7 @@ import android.webkit.JsResult import android.widget.EditText import android.widget.FrameLayout import androidx.core.net.toUri +import com.vitorpamplona.amethyst.commons.R as CommonsR /** * Shows a web page's `alert()` / `confirm()` / `prompt()` / `beforeunload` dialogs for the full-screen @@ -116,9 +117,9 @@ internal class BrowserJsDialogs( */ fun beforeUnload(result: JsResult): Boolean = show(result, onBlocked = { result.confirm() }) { answer -> - setTitle(R.string.napplet_js_leave_title) - setMessage(R.string.napplet_js_leave_message) - setPositiveButton(R.string.napplet_js_leave) { _, _ -> answer { result.confirm() } } + setTitle(CommonsR.string.browser_js_leave_title) + setMessage(CommonsR.string.browser_js_leave_message) + setPositiveButton(CommonsR.string.browser_js_leave) { _, _ -> answer { result.confirm() } } setNegativeButton(android.R.string.cancel) { _, _ -> answer { result.cancel() } } } @@ -151,7 +152,7 @@ internal class BrowserJsDialogs( shownOnThisPage++ val builder = AlertDialog.Builder(activity).apply { build(answer) } if (shownOnThisPage > 1) { - builder.setNeutralButton(R.string.napplet_js_dialog_block) { _, _ -> + builder.setNeutralButton(CommonsR.string.browser_js_dialog_block) { _, _ -> blocked = true answer { onBlocked() } } @@ -173,9 +174,9 @@ internal class BrowserJsDialogs( val scheme = uri?.scheme?.lowercase() val host = uri?.host?.takeIf { it.isNotBlank() } return if (host != null && (scheme == "http" || scheme == "https")) { - activity.getString(R.string.napplet_js_dialog_title, host) + activity.getString(CommonsR.string.browser_js_dialog_title, host) } else { - activity.getString(R.string.napplet_js_dialog_title_generic) + activity.getString(CommonsR.string.browser_js_dialog_title_generic) } } } diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserPopups.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserPopups.kt new file mode 100644 index 0000000000..3a2bbd1c07 --- /dev/null +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserPopups.kt @@ -0,0 +1,137 @@ +/* + * 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.napplethost + +import android.content.Context +import android.content.MutableContextWrapper +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.webkit.WebView +import androidx.webkit.JavaScriptReplyProxy +import androidx.webkit.WebMessageCompat +import androidx.webkit.WebViewCompat +import com.vitorpamplona.amethyst.commons.napplet.NappletWebContract +import java.util.UUID + +/** + * New windows a page opens (`target="_blank"` links and user-initiated `window.open()`), handed from the + * opener's WebView to the full-screen browser window that will show them — Chrome opens these as a new + * tab, we open a new [NappletBrowserActivity] task. + * + * WebView requires the popup's WebView to exist *inside* `onCreateWindow`, before the Activity that will + * show it has started, and that WebView is what keeps `window.opener` / `postMessage` wired for OAuth + * popups. So the opener builds it here, on a [MutableContextWrapper] (re-pointed at the adopting Activity + * later), with the same storage profile, settings and scripts as any browser WebView, and parks it under a + * one-shot token passed in the launch intent. Bridge messages that arrive before the Activity adopts it are + * queued, then replayed. Unclaimed popups are destroyed after [CLAIM_TIMEOUT_MS]. + * + * Opener and popup always share the `:napplet` process (both browser surfaces live there), which is what + * makes handing a live WebView across possible. + */ +object BrowserPopups { + private const val CLAIM_TIMEOUT_MS = 30_000L + + fun interface BridgeTarget { + fun onMessage( + view: WebView, + message: WebMessageCompat, + sourceOrigin: Uri, + isMainFrame: Boolean, + replyProxy: JavaScriptReplyProxy, + ) + } + + class Pending internal constructor( + val webView: WebView, + val context: MutableContextWrapper, + val proxyPort: Int, + val useTor: Boolean, + val themeType: String, + val webViewProfile: String?, + ) { + private var target: BridgeTarget? = null + private val queued = mutableListOf<() -> Unit>() + + internal fun dispatch( + view: WebView, + message: WebMessageCompat, + sourceOrigin: Uri, + isMainFrame: Boolean, + replyProxy: JavaScriptReplyProxy, + ) { + val t = target + if (t != null) { + t.onMessage(view, message, sourceOrigin, isMainFrame, replyProxy) + } else { + queued += { target?.onMessage(view, message, sourceOrigin, isMainFrame, replyProxy) } + } + } + + /** Called by the adopting Activity: from now on bridge messages go to [bridge]; queued ones replay. */ + fun adopt(bridge: BridgeTarget) { + target = bridge + queued.toList().forEach { it() } + queued.clear() + } + } + + private val pending = mutableMapOf<String, Pending>() + private val main = Handler(Looper.getMainLooper()) + + /** + * Builds the popup WebView for `onCreateWindow` and parks it. Returns the token for the launch intent and + * the WebView to put on the `WebViewTransport`. + */ + fun create( + context: Context, + shimJs: String, + proxyPort: Int, + useTor: Boolean, + themeType: String, + webViewProfile: String?, + ): Pair<String, WebView> { + val app = context.applicationContext + val wrapper = MutableContextWrapper(nightThemedContext(app, themeType)) + val webView = WebView(wrapper) + // Same partition as the opener: WebView only links a popup to its opener within one profile, and + // the popup must see the same logged-in session anyway. + NappletWebViewProfile.apply(app, webView, webViewProfile) + BrowserWebTools.applyBrowserSettings(webView) + val entry = Pending(webView, wrapper, proxyPort, useTor, themeType, webViewProfile) + WebViewCompat.addWebMessageListener(webView, NappletWebContract.BRIDGE_NAME, setOf("*")) { view, message, origin, isMainFrame, reply -> + entry.dispatch(view, message, origin, isMainFrame, reply) + } + WebViewCompat.addDocumentStartJavaScript(webView, BrowserWebTools.browserStartScript(shimJs, imeProxy = false), setOf("*")) + val token = UUID.randomUUID().toString() + pending[token] = entry + main.postDelayed({ + pending.remove(token)?.let { orphan -> + orphan.webView.stopLoading() + orphan.webView.destroy() + } + }, CLAIM_TIMEOUT_MS) + return token to webView + } + + /** Hands the parked popup to the Activity that was launched for [token] (once). */ + fun take(token: String?): Pending? = token?.let { pending.remove(it) } +} diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserWebTools.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserWebTools.kt new file mode 100644 index 0000000000..127bedaeda --- /dev/null +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserWebTools.kt @@ -0,0 +1,376 @@ +/* + * 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.napplethost + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.ClipData +import android.content.ClipboardManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.net.http.SslCertificate +import android.os.Build +import android.os.SystemClock +import android.webkit.CookieManager +import android.webkit.WebSettings +import android.webkit.WebStorage +import android.webkit.WebView +import android.widget.Toast +import androidx.core.net.toUri +import androidx.webkit.WebViewCompat +import androidx.webkit.WebViewFeature +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.quartz.utils.Log +import java.net.URISyntaxException +import java.text.DateFormat +import java.util.WeakHashMap +import com.vitorpamplona.amethyst.commons.R as CommonsR + +/** + * WebView-side behaviours shared by the full-screen browser ([NappletBrowserActivity]) and the embedded + * one ([NappletBrowserService]), so the two surfaces act the same: non-web schemes, desktop mode, text + * size, the out-of-scope "back to app" walk, site-data clearing, page info, copy and share. + * + * Everything here runs in the keyless `:napplet` process and touches only the WebView it is given. + */ +object BrowserWebTools { + private const val TAG = "BrowserWebTools" + + // ---- setup ---- + + /** + * The settings every browser WebView gets (full-screen, embedded, and popups), so a page behaves the + * same wherever it opens. New windows are enabled — `onCreateWindow` turns them into new browser + * windows — but `window.open()` still needs a user gesture (Blink's popup blocker, as in Chrome). + * Geolocation is enabled at the WebView level; every request still goes through the per-site prompt. + */ + @Suppress("SetJavaScriptEnabled") + fun applyBrowserSettings(webView: WebView) { + webView.settings.apply { + javaScriptEnabled = true + domStorageEnabled = true + @Suppress("DEPRECATION") + databaseEnabled = false + allowFileAccess = false + allowContentAccess = false + @Suppress("DEPRECATION") + allowFileAccessFromFileURLs = false + @Suppress("DEPRECATION") + allowUniversalAccessFromFileURLs = false + javaScriptCanOpenWindowsAutomatically = false + setSupportMultipleWindows(true) + setGeolocationEnabled(true) + mediaPlaybackRequiresUserGesture = true + builtInZoomControls = true + displayZoomControls = false + loadWithOverviewMode = true + useWideViewPort = true + mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE + if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLE)) { + safeBrowsingEnabled = true + } + } + WebView.setWebContentsDebuggingEnabled(false) + } + + /** + * The document-start script for a browser WebView: the direct-bridge flags, the NIP-07 [shimJs], and + * [BrowserExtrasScript]. [imeProxy] is set only for the embedded surface, which has no native keyboard. + */ + fun browserStartScript( + shimJs: String, + imeProxy: Boolean, + ): String { + val flags = if (imeProxy) " window.__nappletImeProxy = true;" else "" + return "if (window.top === window) { window.__nappletDirectBridge = true; window.__nappletNip07 = true;$flags }\n$shimJs\n${BrowserExtrasScript.JS}" + } + + // ---- non-web schemes ---- + + /** + * Handles a navigation to a non-http(s) [uri] the way Chrome does: `intent:` URIs are parsed (component + * and selector stripped so a page can't aim at a private activity) and fall back to their + * `browser_fallback_url` in-page when no app takes them; other schemes (`mailto:`, `tel:`, `geo:`, + * `nostr:`, …) go to the system. Only acts on a user gesture, like Chrome, so a page can't bounce the + * user into another app on load. Always consumes the navigation. + */ + fun openExternal( + context: Context, + uri: Uri, + hasGesture: Boolean, + loadInPage: (String) -> Unit, + ): Boolean { + if (!hasGesture) return true + val intent = + if (uri.scheme.equals("intent", ignoreCase = true)) { + parseIntentUri(uri.toString()) ?: return true + } else { + Intent(Intent.ACTION_VIEW, uri) + } + intent.addCategory(Intent.CATEGORY_BROWSABLE) + if (context !is Activity) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + try { + context.startActivity(intent) + } catch (_: ActivityNotFoundException) { + val fallback = intent.getStringExtra("browser_fallback_url") + if (fallback != null && (fallback.startsWith("https://") || fallback.startsWith("http://"))) { + loadInPage(fallback) + } + } catch (e: Exception) { + Log.w(TAG, "Could not open ${uri.scheme} link", e) + } + return true + } + + /** Parses an `intent:` URI with the same hardening Chrome applies. Null when it's malformed. */ + fun parseIntentUri(uri: String): Intent? = + try { + Intent.parseUri(uri, Intent.URI_INTENT_SCHEME).apply { + // A web page may only ask for something any app could handle: never an explicit + // component, never a selector (which could smuggle one in). + component = null + selector = null + // No grants of our own content to whatever answers. + flags = flags and + ( + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or + Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or + Intent.FLAG_GRANT_PREFIX_URI_PERMISSION + ).inv() + } + } catch (_: URISyntaxException) { + null + } + + // ---- desktop site / text size ---- + + private val mobileUserAgents = WeakHashMap<WebView, String>() + + fun isDesktopMode(webView: WebView): Boolean = mobileUserAgents.containsKey(webView) + + /** Switches [webView] between its own mobile UA and [BrowserChrome.desktopUserAgent], then reloads. */ + fun setDesktopMode( + webView: WebView, + desktop: Boolean, + ) { + if (desktop == isDesktopMode(webView)) return + val settings = webView.settings + if (desktop) { + val mobile = settings.userAgentString + mobileUserAgents[webView] = mobile + settings.userAgentString = BrowserChrome.desktopUserAgent(mobile) + settings.useWideViewPort = true + settings.loadWithOverviewMode = true + } else { + settings.userAgentString = mobileUserAgents.remove(webView) + } + webView.reload() + } + + fun setTextZoom( + webView: WebView, + percent: Int, + ) { + webView.settings.textZoom = percent.coerceIn(BrowserChrome.TEXT_ZOOM_STEPS.first(), BrowserChrome.TEXT_ZOOM_STEPS.last()) + } + + // ---- scope ---- + + /** + * Chrome's out-of-scope bar ✕: step back to the most recent history entry on [startUrl]'s origin, or + * reload [startUrl] when there is none. + */ + fun backToScope( + webView: WebView, + startUrl: String, + ) { + val home = BrowserChrome.originOf(startUrl) ?: return + val history = webView.copyBackForwardList() + for (i in history.currentIndex - 1 downTo 0) { + if (BrowserChrome.originOf(history.getItemAtIndex(i).url).equals(home, ignoreCase = true)) { + webView.goBackOrForward(i - history.currentIndex) + return + } + } + webView.loadUrl(startUrl) + } + + // ---- storage ---- + + /** The cookie jar of [webView]'s own storage profile (the per-account one), or the default jar. */ + fun cookieManager(webView: WebView): CookieManager = + if (WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROFILE)) { + runCatching { WebViewCompat.getProfile(webView).cookieManager }.getOrNull() ?: CookieManager.getInstance() + } else { + CookieManager.getInstance() + } + + private fun webStorage(webView: WebView): WebStorage = + if (WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROFILE)) { + runCatching { WebViewCompat.getProfile(webView).webStorage }.getOrNull() ?: WebStorage.getInstance() + } else { + WebStorage.getInstance() + } + + /** + * Clears what the site behind [url] stored in [webView]'s profile — its origin's web storage (local + * storage, IndexedDB, cache storage, service workers) and its cookies — then reloads it logged out. + * Other sites and other accounts are untouched. + */ + fun clearSiteData( + context: Context, + webView: WebView, + url: String, + ) { + val origin = BrowserChrome.originOf(url) ?: return + runCatching { webStorage(webView).deleteOrigin(origin) } + val cookies = cookieManager(webView) + val names = + cookies + .getCookie(url) + .orEmpty() + .split(';') + .mapNotNull { it.substringBefore('=').trim().takeIf(String::isNotEmpty) } + val host = BrowserChrome.displayHost(url) + // A cookie can be scoped to the host or to any parent domain; expire it on each so it really goes. + val domains = host.split('.').let { parts -> (0 until (parts.size - 1).coerceAtLeast(1)).map { parts.drop(it).joinToString(".") } } + names.forEach { name -> + cookies.setCookie(url, "$name=; Max-Age=0; Path=/") + domains.forEach { domain -> cookies.setCookie(url, "$name=; Max-Age=0; Path=/; Domain=$domain") } + } + cookies.flush() + Toast.makeText(context, CommonsR.string.browser_site_data_cleared, Toast.LENGTH_SHORT).show() + webView.reload() + } + + // ---- page info ---- + + /** The paragraphs of the page-info sheet for the page in [webView]. */ + fun pageInfo( + context: Context, + webView: WebView, + torOn: Boolean?, + ): String { + val url = webView.url.orEmpty() + val lines = mutableListOf<String>() + lines += + context.getString( + if (url.startsWith("https://", ignoreCase = true)) CommonsR.string.browser_page_info_https else CommonsR.string.browser_page_info_http, + ) + if (torOn != null) { + lines += context.getString(if (torOn) CommonsR.string.browser_page_info_tor else CommonsR.string.browser_page_info_open_web) + } + webView.certificate?.let { lines += certificateLine(context, it) } + return lines.joinToString("\n\n") + } + + private fun certificateLine( + context: Context, + cert: SslCertificate, + ): String { + val to = cert.issuedTo?.cName?.takeIf { it.isNotBlank() } ?: cert.issuedTo?.oName.orEmpty() + val by = cert.issuedBy?.oName?.takeIf { it.isNotBlank() } ?: cert.issuedBy?.cName.orEmpty() + val until = cert.validNotAfterDate?.let { DateFormat.getDateInstance(DateFormat.MEDIUM).format(it) }.orEmpty() + return context.getString(CommonsR.string.browser_page_info_certificate, to, by, until) + } + + // ---- copy / share / other browser ---- + + fun copyToClipboard( + context: Context, + text: String, + ) { + val clipboard = context.getSystemService(ClipboardManager::class.java) ?: return + clipboard.setPrimaryClip(ClipData.newPlainText(text, text)) + // Android 13+ shows its own clipboard confirmation; a toast on top of it would be noise. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + Toast.makeText(context, CommonsR.string.browser_link_copied, Toast.LENGTH_SHORT).show() + } + } + + private var lastShareAt = 0L + + /** + * Opens the Android share sheet for a page or a `navigator.share()` call. Throttled: a page can post + * share requests straight to the bridge (bypassing the polyfill's user-activation check), so at most + * one sheet per [SHARE_COOLDOWN_MS] is honoured. + */ + fun share( + context: Context, + title: String?, + text: String?, + url: String?, + ) { + val now = SystemClock.elapsedRealtime() + if (now - lastShareAt < SHARE_COOLDOWN_MS) return + lastShareAt = now + val body = listOfNotNull(text?.takeIf { it.isNotBlank() }, url?.takeIf { it.isNotBlank() }).joinToString("\n") + if (body.isEmpty()) return + val send = + Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, body) + title?.takeIf { it.isNotBlank() }?.let { putExtra(Intent.EXTRA_SUBJECT, it) } + } + val chooser = Intent.createChooser(send, context.getString(CommonsR.string.browser_share_chooser)) + if (context !is Activity) chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + runCatching { context.startActivity(chooser) }.onFailure { Log.w(TAG, "Share failed", it) } + } + + /** + * Hands [url] to a browser other than Amethyst — the escape hatch for sites that refuse embedded + * browsers (Google sign-in, some banks). Our own activities are excluded from the chooser. + */ + fun openInOtherBrowser( + context: Context, + url: String, + ) { + val view = Intent(Intent.ACTION_VIEW, url.toUri()).addCategory(Intent.CATEGORY_BROWSABLE) + val chooser = + Intent.createChooser(view, null).apply { + putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, ownBrowsableComponents(context, view)) + if (context !is Activity) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + try { + context.startActivity(chooser) + } catch (_: ActivityNotFoundException) { + Toast.makeText(context, CommonsR.string.browser_no_other_browser, Toast.LENGTH_SHORT).show() + } + } + + private fun ownBrowsableComponents( + context: Context, + intent: Intent, + ): Array<ComponentName> = + runCatching { + @Suppress("DEPRECATION") + context.packageManager + .queryIntentActivities(intent, 0) + .filter { it.activityInfo.packageName == context.packageName } + .map { ComponentName(it.activityInfo.packageName, it.activityInfo.name) } + .toTypedArray() + }.getOrDefault(emptyArray()) + + private const val SHARE_COOLDOWN_MS = 1_000L +} diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt index 473ccb0e6f..13b8c3069d 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt @@ -20,33 +20,45 @@ */ package com.vitorpamplona.amethyst.napplethost +import android.Manifest +import android.app.ActivityManager +import android.app.AlertDialog import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection +import android.content.pm.PackageManager import android.content.res.ColorStateList import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.drawable.Icon import android.net.Uri +import android.os.Build import android.os.Bundle import android.os.Handler import android.os.IBinder import android.os.Looper import android.os.Message import android.os.Messenger +import android.util.TypedValue +import android.view.ContextMenu import android.view.Gravity import android.view.View import android.view.ViewGroup import android.webkit.ConsoleMessage +import android.webkit.GeolocationPermissions import android.webkit.JsPromptResult import android.webkit.JsResult +import android.webkit.PermissionRequest +import android.webkit.RenderProcessGoneDetail import android.webkit.ValueCallback import android.webkit.WebChromeClient import android.webkit.WebResourceError import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse -import android.webkit.WebSettings import android.webkit.WebView import android.webkit.WebViewClient +import android.widget.Button import android.widget.FrameLayout import android.widget.LinearLayout import android.widget.ProgressBar @@ -54,15 +66,24 @@ import android.widget.TextView import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.OnBackPressedCallback +import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat +import androidx.core.graphics.ColorUtils import androidx.core.graphics.scale import androidx.core.net.toUri +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat import androidx.webkit.JavaScriptReplyProxy import androidx.webkit.ProxyConfig import androidx.webkit.ProxyController import androidx.webkit.WebMessageCompat import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.Action +import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission +import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission.Decision import com.vitorpamplona.amethyst.commons.browser.OmniboxInput import com.vitorpamplona.amethyst.commons.napplet.NappletWebContract import com.vitorpamplona.amethyst.commons.util.parseJsonObjectOrNull @@ -77,35 +98,50 @@ import com.vitorpamplona.amethyst.commons.R as CommonsR /** * Full-screen **direct-WebView** browser for an arbitrary URL, running in the keyless `:napplet` - * process. Unlike the embedded browser ([NappletBrowserService], which streams its surface to the main - * app through SurfaceControlViewHost — a path that, on current Android, forwards taps but drops scroll/ - * zoom/keyboard gestures), this hosts the WebView **directly** in its own window, so scrolling, pinch - * zoom, and the soft keyboard all work natively — the window insets the content for the IME itself (see - * [applyFullScreenHostInsets]). It stays just as keyless: the page JS runs here, every NIP-07 - * `window.nostr` call is brokered + consent-gated in the main process per origin, and the keys never - * leave it. + * process — Amethyst's equivalent of an installed Chrome PWA window. Unlike the embedded browser + * ([NappletBrowserService], which streams its surface to the main app through SurfaceControlViewHost — a + * path that, on current Android, forwards taps but drops scroll/zoom/keyboard gestures), this hosts the + * WebView **directly** in its own window, so scrolling, pinch zoom, and the soft keyboard all work natively + * — the window insets the content for the IME itself (see [applyFullScreenHostInsets]). It stays just as + * keyless: the page JS runs here, every NIP-07 `window.nostr` call is brokered + consent-gated in the main + * process per origin, and the keys never leave it. * - * Mirrors [NappletHostActivity]'s sandbox scaffolding (trusted chrome, loading screen, foreground hold) - * but loads a live URL directly instead of serving verified blobs through a shell. + * PWA behaviours, beyond the page itself: its own task in Recents titled, iconed and coloured after the + * site ([updateTaskDescription]); system bars tinted with the page's `theme-color`; the top pill + * ([NappletControlSheet], laid out by [BrowserChrome]); JS dialogs; new windows (`_blank` / `window.open`) + * as new browser windows with `opener` intact ([BrowserPopups]); downloads; HTML fullscreen video; + * camera / microphone / location behind a per-site prompt; find in page; long-press link and image menus; + * Web Share; and recovery from a renderer crash. */ class NappletBrowserActivity : ComponentActivity() { - private lateinit var webView: WebView + private var webView: WebView? = null private var startUrl: String = "about:blank" private var proxyPort: Int = -1 private var useTor: Boolean = true private var themeType: String = "SYSTEM" + private var webViewProfile: String? = null + + // Key for this window's foreground lease with the broker; stable for the Activity's life. + private var leaseKey: String = "" private val contentFrame by lazy { FrameLayout(this) } + private var root: FrameLayout? = null private var loadingView: View? = null + private var crashView: View? = null private var resumed = false private var controlSheet: NappletControlSheet? = null private var consolePanel: NappletConsolePanel? = null + private var findBar: BrowserFindBar? = null + private var consoleShowing = false // A thin determinate progress bar pinned to the top edge (browser-style), driven by the chrome // client's onProgressChanged; hidden at 100%. private val topProgressBar by lazy { buildTopProgressBar() } + // The NIP-07 shim + browser extras, injected at document start in this window and in its popups. + private var shimJs: String = "" + // Visit-history gating: only a clean main-frame load (no error) is recorded, so a misspelled/ // unresolved address never enters history. Reset on each main-frame page start. private var pendingMainFrameUrl: String? = null @@ -116,6 +152,15 @@ class NappletBrowserActivity : ComponentActivity() { // same address don't each trigger a round-trip. private var lastFavoriteQueryUrl: String? = null + // What Recents shows for this task: the page's title, favicon and theme colour. + private var pageTitle: String? = null + private var pageIcon: Bitmap? = null + private var themeColor: Int? = null + + // HTML fullscreen (a video's fullscreen button): the view WebView hands us, drawn over the whole window. + private var customView: View? = null + private var customViewCallback: WebChromeClient.CustomViewCallback? = null + // Page-originated alert/confirm/prompt/beforeunload, labelled with the page's origin. private val jsDialogs by lazy { BrowserJsDialogs(this) } @@ -127,6 +172,17 @@ class NappletBrowserActivity : ComponentActivity() { private val fileChooserLauncher = WebFileChooserLauncher(this) { uris -> pendingFileChooser.deliver(uris) } + // ---- site permissions (camera / microphone / location) ---- + private val sitePermissionQueries = mutableMapOf<Long, (Map<BrowserSitePermission, Decision>) -> Unit>() + private var sitePermissionSeq = 0L + private var permissionPrompt: AlertDialog? = null + private var pendingRuntimeGrant: ((Map<String, Boolean>) -> Unit)? = null + private val runtimePermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> + pendingRuntimeGrant?.invoke(result) + pendingRuntimeGrant = null + } + // ---- broker bridge (per-origin NIP-07 tokens; identical to NappletBrowserService) ---- private var brokerMessenger: Messenger? = null @@ -163,20 +219,30 @@ class NappletBrowserActivity : ComponentActivity() { private val pendingByOrigin = mutableMapOf<String, MutableList<Message>>() private val mintInFlight = mutableSetOf<String>() + /** + * Back walks out of fullscreen video, then the find bar, then the page's history, then leaves. Enabled + * only while one of those applies, so the system back (and its predictive animation) otherwise acts + * on the window itself. + */ private val backCallback = object : OnBackPressedCallback(false) { override fun handleOnBackPressed() { - if (this@NappletBrowserActivity::webView.isInitialized && webView.canGoBack()) { - webView.goBack() - } else { - isEnabled = false - onBackPressedDispatcher.onBackPressed() + val wv = webView + when { + customView != null -> exitFullscreen() + findBar?.isShowing == true -> findBar?.hide() + wv != null && wv.canGoBack() -> wv.goBack() + else -> { + isEnabled = false + onBackPressedDispatcher.onBackPressed() + } } + syncBackState() } } private fun syncBackState() { - if (this::webView.isInitialized) backCallback.isEnabled = webView.canGoBack() + backCallback.isEnabled = customView != null || findBar?.isShowing == true || webView?.canGoBack() == true } private val brokerConnection = @@ -199,14 +265,32 @@ class NappletBrowserActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - startUrl = intent.getStringExtra(EXTRA_URL)?.takeIf { it.isNotBlank() } ?: run { + // A new window a page opened: its WebView already exists (built inside the opener's onCreateWindow). + val popupToken = intent.getStringExtra(EXTRA_POPUP_TOKEN) + val popup = BrowserPopups.take(popupToken) + if (popupToken != null && popup == null) { finish() return } - proxyPort = intent.getIntExtra(EXTRA_PROXY_PORT, -1) - useTor = intent.getBooleanExtra(EXTRA_USE_TOR, true) + + if (popup != null) { + proxyPort = popup.proxyPort + useTor = popup.useTor + themeType = popup.themeType + webViewProfile = popup.webViewProfile + leaseKey = "popup:$popupToken" + } else { + startUrl = intent.getStringExtra(EXTRA_URL)?.takeIf { it.isNotBlank() } ?: run { + finish() + return + } + proxyPort = intent.getIntExtra(EXTRA_PROXY_PORT, -1) + useTor = intent.getBooleanExtra(EXTRA_USE_TOR, true) + themeType = intent.getStringExtra(EXTRA_THEME).orEmpty().ifBlank { "SYSTEM" } + webViewProfile = intent.getStringExtra(NappletHostContract.EXTRA_WEBVIEW_PROFILE) + leaseKey = startUrl + } title = intent.getStringExtra(EXTRA_TITLE).orEmpty() - themeType = intent.getStringExtra(EXTRA_THEME).orEmpty().ifBlank { "SYSTEM" } if (!WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { Toast.makeText(this, getString(R.string.napplet_webview_too_old), Toast.LENGTH_LONG).show() @@ -214,61 +298,80 @@ class NappletBrowserActivity : ComponentActivity() { return } - // Build the WebView from a context forced to the app theme so its content follows DARK/LIGHT even when - // the device theme differs (WebView reads the context's theme, not the window's — see nightThemedContext). - webView = WebView(nightThemedContext(this, themeType)) - // FIRST touch after construction: setProfile throws once the WebView has loaded content (or its - // profile has otherwise been used), so the storage partition must be chosen before anything else. - NappletWebViewProfile.apply(this, webView, intent.getStringExtra(NappletHostContract.EXTRA_WEBVIEW_PROFILE)) - configureWebView(webView) - webView.setBackgroundColor(resolveThemeColor(android.R.attr.colorBackground)) - webView.dropSystemBarInsets() + shimJs = readContractAsset(NappletWebContract.SHIM_JS_PATH).decodeToString() applyWebViewProxy(if (useTor) proxyPort else -1) - // NIP-07 over the direct bridge (no shell): the shim talks to native at document start for every - // origin; the broker scopes consent per visited origin. - val shim = readContractAsset(NappletWebContract.SHIM_JS_PATH).decodeToString() - WebViewCompat.addWebMessageListener(webView, NappletWebContract.BRIDGE_NAME, setOf("*"), ::onBridgeMessage) - val startScript = "if (window.top === window) { window.__nappletDirectBridge = true; window.__nappletNip07 = true; }\n$shim" - WebViewCompat.addDocumentStartJavaScript(webView, startScript, setOf("*")) - bindService(Intent().setClassName(this, NappletHostContract.BROKER_SERVICE_CLASS), brokerConnection, BIND_AUTO_CREATE) onBackPressedDispatcher.addCallback(this, backCallback) + val findBar = BrowserFindBar(this, { webView }) { syncBackState() }.also { this.findBar = it } val root = FrameLayout(this).apply { + setBackgroundColor(resolveThemeColor(android.R.attr.colorBackground)) addView(contentFrame, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)) - addView( - buildControlSheet(), - FrameLayout - .LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.WRAP_CONTENT, - Gravity.TOP, - ), - ) - addView( - buildConsolePanel(), - FrameLayout - .LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.WRAP_CONTENT, - Gravity.BOTTOM, - ), - ) + addView(buildControlSheet(), FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.TOP)) + addView(buildConsolePanel(), FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM)) + addView(findBar, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM)) // Added last so the thin loading bar paints above the content (and over the grabber's top edge). addView(topProgressBar) } + this.root = root setContentView(root) // Pad by the system bars + cutout AND the IME: on an edge-to-edge window (enforced for targetSdk // 35+ on Android 15+) windowSoftInputMode=adjustResize no longer shrinks the window, so without - // this the keyboard covers the bottom of the page. See applyFullScreenHostInsets. + // this the keyboard covers the bottom of the page. See applyFullScreenHostInsets. The root's own + // background shows through that padding, which is how the page's theme-color tints the bars. root.applyFullScreenHostInsets() - contentFrame.addView(webView, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)) - loadingView = buildLoadingView().also { contentFrame.addView(it) } + val wv = buildWebView(popup) + contentFrame.addView(wv, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)) + if (popup == null) { + loadingView = buildLoadingView().also { contentFrame.addView(it) } + wv.loadUrl(startUrl) + } else { + wv.url?.let { if (it.isNotBlank() && it != "about:blank") startUrl = it } + } + updateTaskDescription() + } - webView.loadUrl(startUrl) + /** + * Builds (or, for a popup, adopts) this window's WebView and wires every client and bridge. Also used + * to rebuild after a renderer crash. + */ + private fun buildWebView(popup: BrowserPopups.Pending? = null): WebView { + val wv: WebView + if (popup != null) { + wv = popup.webView + // The popup was built on a placeholder context; point it at this Activity now. + popup.context.baseContext = nightThemedContext(this, themeType) + } else { + // Build the WebView from a context forced to the app theme so its content follows DARK/LIGHT + // even when the device theme differs (WebView reads the context's theme, not the window's). + wv = WebView(nightThemedContext(this, themeType)) + // FIRST touch after construction: setProfile throws once the WebView has loaded content (or + // its profile has otherwise been used), so the storage partition must be chosen first. + NappletWebViewProfile.apply(this, wv, webViewProfile) + } + BrowserWebTools.applyBrowserSettings(wv) + wv.webViewClient = BrowserClient() + wv.webChromeClient = BrowserChromeClient() + wv.setDownloadListener { url, userAgent, contentDisposition, mimeType, _ -> + BrowserDownloads.download(this, url, userAgent, contentDisposition, mimeType, BrowserWebTools.cookieManager(wv).getCookie(url), if (useTor) proxyPort else -1) + } + wv.setBackgroundColor(resolveThemeColor(android.R.attr.colorBackground)) + wv.dropSystemBarInsets() + registerForContextMenu(wv) + + if (popup != null) { + popup.adopt(::onBridgeMessage) + } else { + // NIP-07 over the direct bridge (no shell): the shim talks to native at document start for + // every origin; the broker scopes consent per visited origin. + WebViewCompat.addWebMessageListener(wv, NappletWebContract.BRIDGE_NAME, setOf("*"), ::onBridgeMessage) + WebViewCompat.addDocumentStartJavaScript(wv, BrowserWebTools.browserStartScript(shimJs, imeProxy = false), setOf("*")) + } + webView = wv + return wv } // Renews the broker's foreground lease while resumed; without it the broker's watchdog would reap the @@ -284,22 +387,18 @@ class NappletBrowserActivity : ComponentActivity() { override fun onResume() { super.onResume() - if (this::webView.isInitialized) { - webView.onResume() - } + webView?.onResume() resumed = true heartbeatHandler.removeCallbacks(heartbeat) heartbeat.run() } override fun onPause() { - if (this::webView.isInitialized) { - // Only pause THIS activity's WebView (onPause is per-WebView). Do NOT call pauseTimers(): it is - // process-global — it freezes JS/layout/parsing timers for EVERY WebView in `:napplet`, including - // the embedded ones in NappletBrowserService, which have no resume of their own. That left the - // embed frozen (dead page/connection) after returning from a full-screen excursion. - webView.onPause() - } + // Only pause THIS activity's WebView (onPause is per-WebView). Do NOT call pauseTimers(): it is + // process-global — it freezes JS/layout/parsing timers for EVERY WebView in `:napplet`, including + // the embedded ones in NappletBrowserService, which have no resume of their own. That left the + // embed frozen (dead page/connection) after returning from a full-screen excursion. + webView?.onPause() resumed = false heartbeatHandler.removeCallbacks(heartbeat) setBrokerForeground(false) @@ -317,27 +416,35 @@ class NappletBrowserActivity : ComponentActivity() { fileChooserLauncher.teardown() // A dialog still up would leak its window and leave the page's JS blocked on an unanswered result. jsDialogs.dismiss() - if (this::webView.isInitialized) { - // Detach from the view tree BEFORE destroy(). Destroying a WebView while it is still attached to - // the window corrupts the SHARED multiprocess renderer/network state, which then breaks the OTHER - // (embedded) WebViews living in this `:napplet` process: dead DNS (ERR_NAME_NOT_RESOLVED), DOM reads - // returning empty (`value == ""` on a field that visibly shows text), dead selection-highlight paint, - // and broken IME — all after a full-screen excursion returns to an embed. (`destroy()` requires the - // view to be removed from the hierarchy first; see WebView.destroy() docs.) - webView.stopLoading() - (webView.parent as? ViewGroup)?.removeView(webView) - webView.destroy() - } + permissionPrompt?.dismiss() + customViewCallback?.onCustomViewHidden() + destroyWebView() super.onDestroy() } + /** + * Detaches, then destroys, the WebView. Destroying a WebView while it is still attached to the window + * corrupts the SHARED multiprocess renderer/network state, which then breaks the OTHER (embedded) + * WebViews living in this `:napplet` process: dead DNS (ERR_NAME_NOT_RESOLVED), DOM reads returning + * empty, dead selection-highlight paint, and broken IME — all after a full-screen excursion returns to + * an embed. (`destroy()` requires the view to be removed from the hierarchy first.) + */ + private fun destroyWebView() { + val wv = webView ?: return + webView = null + unregisterForContextMenu(wv) + wv.stopLoading() + (wv.parent as? ViewGroup)?.removeView(wv) + wv.destroy() + } + /** Reports foreground state to the broker so the main process stays resumed (Tor/relays/AUTH). */ private fun setBrokerForeground(foreground: Boolean) { val msg = Message.obtain(null, NappletIpc.MSG_SET_FOREGROUND).apply { data = Bundle().apply { - putString(NappletIpc.KEY_LAUNCH_TOKEN, startUrl) + putString(NappletIpc.KEY_LAUNCH_TOKEN, leaseKey) putBoolean(NappletIpc.KEY_FOREGROUND, foreground) } } @@ -354,43 +461,14 @@ class NappletBrowserActivity : ComponentActivity() { val msg = Message.obtain(null, NappletIpc.MSG_RELEASE_CLIENT).apply { replyTo = replyMessenger - data = Bundle().apply { putString(NappletIpc.KEY_LAUNCH_TOKEN, startUrl) } + data = Bundle().apply { putString(NappletIpc.KEY_LAUNCH_TOKEN, leaseKey) } } runCatching { broker.send(msg) } } - @Suppress("SetJavaScriptEnabled") - private fun configureWebView(wv: WebView) { - wv.settings.apply { - javaScriptEnabled = true - domStorageEnabled = true - @Suppress("DEPRECATION") - databaseEnabled = false - allowFileAccess = false - allowContentAccess = false - @Suppress("DEPRECATION") - allowFileAccessFromFileURLs = false - @Suppress("DEPRECATION") - allowUniversalAccessFromFileURLs = false - javaScriptCanOpenWindowsAutomatically = false - setSupportMultipleWindows(false) - setGeolocationEnabled(false) - mediaPlaybackRequiresUserGesture = true - builtInZoomControls = true - displayZoomControls = false - loadWithOverviewMode = true - useWideViewPort = true - mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE - if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLE)) { - safeBrowsingEnabled = true - } - } - WebView.setWebContentsDebuggingEnabled(false) - wv.webViewClient = BrowserClient() - wv.webChromeClient = BrowserChromeClient() - } + private fun currentUrl(): String = webView?.url?.takeIf { it.isNotBlank() } ?: controlSheet?.state?.url?.takeIf { it.isNotBlank() } ?: startUrl - /** Captures favicon and console output, drives the top loading bar, and opens the file picker. */ + /** Captures favicon, title and console output, and hosts every page-initiated UI. */ private inner class BrowserChromeClient : WebChromeClient() { /** * Without this override the base implementation returns false and WebView shows nothing at all, so @@ -414,7 +492,35 @@ class NappletBrowserActivity : ComponentActivity() { view: WebView, title: String?, ) { + pageTitle = title?.trim()?.takeIf { it.isNotEmpty() && it != view.url } controlSheet?.updateTitle(title) + updateTaskDescription() + } + + override fun onReceivedIcon( + view: WebView, + icon: Bitmap?, + ) { + if (icon == null || mainFrameLoadFailed) return + pageIcon = icon + updateTaskDescription() + val host = OmniboxInput.hostOf(view.url ?: return) ?: return + // De-dupe: a page can fire this several times — store once per host per visit. + if (host == lastIconHost) return + lastIconHost = host + recordIcon(host, icon) + } + + override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { + val panel = consolePanel ?: return false + panel.appendLog( + consoleMessage.messageLevel(), + consoleMessage.message(), + consoleMessage.sourceId(), + consoleMessage.lineNumber(), + ) + controlSheet?.updateConsoleCount(panel.entryCount) + return true } // The framework's own JS dialogs only appear when the WebView's context IS an Activity @@ -450,29 +556,57 @@ class NappletBrowserActivity : ComponentActivity() { result: JsResult, ): Boolean = jsDialogs.beforeUnload(result) - override fun onReceivedIcon( + /** + * A `_blank` link or a user-initiated `window.open()`: open it as a new browser window, like a new + * Chrome tab. Without a user gesture it's refused (Chrome's popup blocker). + */ + override fun onCreateWindow( view: WebView, - icon: Bitmap?, - ) { - if (icon == null || mainFrameLoadFailed) return - val host = OmniboxInput.hostOf(view.url ?: return) ?: return - // De-dupe: a page can fire this several times — store once per host per visit. - if (host == lastIconHost) return - lastIconHost = host - recordIcon(host, icon) - } - - override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { - val panel = consolePanel ?: return false - panel.appendLog( - consoleMessage.messageLevel(), - consoleMessage.message(), - consoleMessage.sourceId(), - consoleMessage.lineNumber(), - ) - controlSheet?.updateConsoleCount(panel.entryCount) + isDialog: Boolean, + isUserGesture: Boolean, + resultMsg: Message, + ): Boolean { + if (!isUserGesture) return false + val transport = resultMsg.obj as? WebView.WebViewTransport ?: return false + val (token, child) = BrowserPopups.create(this@NappletBrowserActivity, shimJs, proxyPort, useTor, themeType, webViewProfile) + transport.webView = child + resultMsg.sendToTarget() + startActivity(popupIntent(this@NappletBrowserActivity, token)) return true } + + /** `window.close()` from a window a page opened: close this window. */ + override fun onCloseWindow(window: WebView) { + if (window === webView) finish() + } + + override fun onPermissionRequest(request: PermissionRequest) = handlePermissionRequest(request) + + override fun onPermissionRequestCanceled(request: PermissionRequest) { + permissionPrompt?.dismiss() + } + + override fun onGeolocationPermissionsShowPrompt( + origin: String, + callback: GeolocationPermissions.Callback, + ) { + val siteOrigin = BrowserChrome.originOf(origin) ?: return callback.invoke(origin, false, false) + resolveSitePermissions(siteOrigin, listOf(BrowserSitePermission.LOCATION)) { granted -> + // Never let WebView remember it: the answer lives in the main-process registry. + callback.invoke(origin, BrowserSitePermission.LOCATION in granted, false) + } + } + + override fun onGeolocationPermissionsHidePrompt() { + permissionPrompt?.dismiss() + } + + override fun onShowCustomView( + view: View, + callback: CustomViewCallback, + ) = enterFullscreen(view, callback) + + override fun onHideCustomView() = exitFullscreen() } /** @@ -503,10 +637,7 @@ class NappletBrowserActivity : ComponentActivity() { val uri = request.url val scheme = uri.scheme?.lowercase() if (scheme == "http" || scheme == "https") return false - if (request.hasGesture()) { - runCatching { startActivity(Intent(Intent.ACTION_VIEW, uri).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) } - } - return true + return BrowserWebTools.openExternal(this@NappletBrowserActivity, uri, request.hasGesture()) { view.loadUrl(it) } } override fun onPageStarted( @@ -517,10 +648,19 @@ class NappletBrowserActivity : ComponentActivity() { // A fresh main-frame navigation: arm history gating and show the new address. pendingMainFrameUrl = url mainFrameLoadFailed = false + // A window a page opened takes its first real page as its home ("scope"). + if (startUrl == "about:blank" && url.startsWith("http")) startUrl = url // Re-arm favicon capture when the host changes, so a same-host in-page nav doesn't re-send. - if (OmniboxInput.hostOf(url) != lastIconHost) lastIconHost = null + if (OmniboxInput.hostOf(url) != lastIconHost) { + lastIconHost = null + pageIcon = null + pageTitle = null + themeColor = null + applyThemeColor(null) + } // Chrome scopes "block this page's dialogs" to the page: a new main-frame load lifts it. jsDialogs.onMainFrameNavigation() + controlSheet?.setLoading(true) showUrl(url) } @@ -558,7 +698,7 @@ class NappletBrowserActivity : ComponentActivity() { url: String, isReload: Boolean, ) { - syncBackState() + syncNavigation(view) showUrl(url) } @@ -566,7 +706,8 @@ class NappletBrowserActivity : ComponentActivity() { view: WebView, url: String, ) { - syncBackState() + syncNavigation(view) + controlSheet?.setLoading(false) showUrl(url) // Record only a clean http(s) main-frame load — never a typed-but-failed address. if (!mainFrameLoadFailed && (url.startsWith("https://") || url.startsWith("http://"))) { @@ -574,6 +715,33 @@ class NappletBrowserActivity : ComponentActivity() { scheduleFaviconSniff(view, url) } } + + /** + * The renderer died (crashed or was killed for memory). Every WebView in `:napplet` shares one + * renderer, and returning false here would kill the whole process — taking every embedded tab with + * it. So drop just this WebView and offer a reload, like Chrome's "Aw, Snap!". + */ + override fun onRenderProcessGone( + view: WebView, + detail: RenderProcessGoneDetail, + ): Boolean { + if (view !== webView) { + (view.parent as? ViewGroup)?.removeView(view) + view.destroy() + return true + } + val lastUrl = view.url?.takeIf { it.startsWith("http") } ?: startUrl + Log.w(TAG) { "Renderer gone (crashed=${detail.didCrash()}); offering a reload of $lastUrl" } + exitFullscreen() + destroyWebView() + showCrashView(lastUrl) + return true + } + } + + private fun syncNavigation(view: WebView) { + syncBackState() + controlSheet?.setNavigation(view.canGoBack(), view.canGoForward()) } /** @@ -589,7 +757,7 @@ class NappletBrowserActivity : ComponentActivity() { replyTo = replyMessenger data = Bundle().apply { putString(NappletIpc.KEY_FAVORITE_URL, url) } } - if (brokerMessenger != null) sendToBroker(msg) else pendingBrokerRequests.add(msg) + queueToBroker(msg) } /** @@ -604,7 +772,7 @@ class NappletBrowserActivity : ComponentActivity() { ) { val host = OmniboxInput.hostOf(url) ?: return view.postDelayed({ - if (mainFrameLoadFailed || host == lastIconHost || view.url != url) return@postDelayed + if (view !== webView || mainFrameLoadFailed || host == lastIconHost || view.url != url) return@postDelayed NappletFaviconSniffer.capture(view) { sniffedHost, bytes -> if (sniffedHost == lastIconHost) return@capture lastIconHost = sniffedHost @@ -626,7 +794,7 @@ class NappletBrowserActivity : ComponentActivity() { putString(NappletIpc.KEY_HISTORY_TITLE, title.orEmpty()) } } - if (brokerMessenger != null) sendToBroker(msg) else pendingBrokerRequests.add(msg) + queueToBroker(msg) } /** Scales [icon] down and relays it to the broker as the favicon for [host] (PNG bytes over IPC). */ @@ -636,12 +804,7 @@ class NappletBrowserActivity : ComponentActivity() { ) { val bytes = runCatching { - val scaled = - if (icon.width > ICON_MAX_PX || icon.height > ICON_MAX_PX) { - icon.scale(ICON_MAX_PX, ICON_MAX_PX) - } else { - icon - } + val scaled = if (icon.width > ICON_MAX_PX || icon.height > ICON_MAX_PX) icon.scale(ICON_MAX_PX, ICON_MAX_PX) else icon ByteArrayOutputStream().use { out -> scaled.compress(Bitmap.CompressFormat.PNG, 100, out) out.toByteArray() @@ -663,17 +826,18 @@ class NappletBrowserActivity : ComponentActivity() { putByteArray(NappletIpc.KEY_ICON_BYTES, bytes) } } - if (brokerMessenger != null) sendToBroker(msg) else pendingBrokerRequests.add(msg) + queueToBroker(msg) } - /** Loads a user-typed address from the in-page address bar, forcing Tor for `.onion` when available. */ + /** Loads a user-typed address from "Edit address", forcing Tor for `.onion` when available. */ private fun loadAddress(text: String) { val resolved = OmniboxInput.resolve(text) ?: return if (resolved.forceTor && proxyPort > 0 && !useTor) { useTor = true applyWebViewProxy(proxyPort) + controlSheet?.setTor(true) } - if (this::webView.isInitialized) webView.loadUrl(resolved.url) + webView?.loadUrl(resolved.url) } // ---- bridge: page <-> native (mirror of NappletBrowserService.onBridgeMessage) ---- @@ -690,6 +854,12 @@ class NappletBrowserActivity : ComponentActivity() { val raw = message.data ?: return val envelope = parseJsonObjectOrNull(raw) ?: return + // Browser conveniences (share, theme colour, blob downloads) are handled here, never brokered. + if (envelope.stringOrNull("type").orEmpty().startsWith("browser.")) { + onBrowserMessage(envelope) + return + } + val scheme = sourceOrigin.scheme ?: return val host = sourceOrigin.host ?: return val origin = "$scheme://$host" + if (sourceOrigin.port > 0) ":${sourceOrigin.port}" else "" @@ -708,13 +878,34 @@ class NappletBrowserActivity : ComponentActivity() { val token = originTokens[origin] if (token != null) { msg.data.putString(NappletIpc.KEY_LAUNCH_TOKEN, token) - if (brokerMessenger == null) pendingBrokerRequests.add(msg) else sendToBroker(msg) + queueToBroker(msg) } else { pendingByOrigin.getOrPut(origin) { mutableListOf() }.add(msg) requestBrowserToken(origin) } } + /** A `browser.*` message from [BrowserExtrasScript]. */ + private fun onBrowserMessage(envelope: JsonObject) { + when (envelope.stringOrNull("type")) { + "browser.share" -> + if (resumed) { + BrowserWebTools.share(this, envelope.stringOrNull("title"), envelope.stringOrNull("text"), envelope.stringOrNull("url")) + } + "browser.themeColor" -> { + themeColor = BrowserChrome.parseCssRgb(envelope.stringOrNull("color")) + applyThemeColor(themeColor) + updateTaskDescription() + } + "browser.download" -> { + val data = envelope.stringOrNull("data") ?: return + if (data.startsWith("data:") && data.length <= BrowserDownloads.MAX_INLINE_BYTES / 3 * 4 + 256) { + BrowserDownloads.saveDataUrl(this, data, envelope.stringOrNull("name")) + } + } + } + } + private fun requestBrowserToken(origin: String) { if (!mintInFlight.add(origin)) return val msg = @@ -722,7 +913,12 @@ class NappletBrowserActivity : ComponentActivity() { replyTo = replyMessenger data = Bundle().apply { putString(NappletIpc.KEY_BROWSER_ORIGIN, origin) } } - if (brokerMessenger == null) pendingBrokerRequests.add(msg) else sendToBroker(msg) + queueToBroker(msg) + } + + /** Sends now when the broker is bound, else queues until it is. */ + private fun queueToBroker(msg: Message) { + if (brokerMessenger != null) sendToBroker(msg) else pendingBrokerRequests.add(msg) } private fun sendToBroker(msg: Message) { @@ -750,6 +946,14 @@ class NappletBrowserActivity : ComponentActivity() { val url = data.getString(NappletIpc.KEY_FAVORITE_URL) ?: return true controlSheet?.setFavorite(url, data.getBoolean(NappletIpc.KEY_FAVORITE_IS_FAVORITE, false)) } + NappletIpc.MSG_SITE_PERMISSIONS -> { + val callback = sitePermissionQueries.remove(data.getLong(NappletIpc.KEY_REQUEST_ID)) ?: return true + callback( + BrowserSitePermission.entries.associateWith { permission -> + runCatching { Decision.valueOf(data.getString(NappletIpc.KEY_SITE_PERMISSION_PREFIX + permission.key).orEmpty()) }.getOrDefault(Decision.ASK) + }, + ) + } NappletIpc.MSG_BROWSER_TOKEN -> { val origin = data.getString(NappletIpc.KEY_BROWSER_ORIGIN) ?: return true val token = data.getString(NappletIpc.KEY_LAUNCH_TOKEN) ?: return true @@ -765,6 +969,298 @@ class NappletBrowserActivity : ComponentActivity() { return true } + // ---- site permissions ---- + + private fun handlePermissionRequest(request: PermissionRequest) { + val origin = BrowserChrome.originOf(request.origin.toString()) ?: return request.deny() + val wanted = request.resources.mapNotNull(::sitePermissionFor).distinct() + if (wanted.isEmpty()) return request.deny() + resolveSitePermissions(origin, wanted) { granted -> + val resources = request.resources.filter { sitePermissionFor(it) in granted }.toTypedArray() + if (resources.isEmpty()) request.deny() else request.grant(resources) + } + } + + private fun sitePermissionFor(resource: String): BrowserSitePermission? = + when (resource) { + PermissionRequest.RESOURCE_VIDEO_CAPTURE -> BrowserSitePermission.CAMERA + PermissionRequest.RESOURCE_AUDIO_CAPTURE -> BrowserSitePermission.MICROPHONE + else -> null + } + + /** + * Decides [wanted] for [origin]: the user's remembered answers first (kept per origin in the main + * process — the same on Tor and the open web), a prompt for anything never answered, and finally + * Android's own runtime permission for whatever was allowed. [done] receives what is granted. + */ + private fun resolveSitePermissions( + origin: String, + wanted: List<BrowserSitePermission>, + done: (Set<BrowserSitePermission>) -> Unit, + ) { + querySitePermissions(origin) { decisions -> + val allowed = wanted.filter { decisions[it] == Decision.ALLOW }.toSet() + val ask = wanted.filter { decisions[it] == Decision.ASK } + if (ask.isEmpty()) { + ensureRuntimePermissions(allowed, done) + } else { + showPermissionPrompt(origin, ask) { allow -> + // null = dismissed without an answer: deny this once, remember nothing. + if (allow != null) ask.forEach { rememberSitePermission(origin, it, if (allow) Decision.ALLOW else Decision.BLOCK) } + ensureRuntimePermissions(if (allow == true) allowed + ask else allowed, done) + } + } + } + } + + private fun querySitePermissions( + origin: String, + callback: (Map<BrowserSitePermission, Decision>) -> Unit, + ) { + val id = ++sitePermissionSeq + sitePermissionQueries[id] = callback + val msg = + Message.obtain(null, NappletIpc.MSG_QUERY_SITE_PERMISSIONS).apply { + replyTo = replyMessenger + data = + Bundle().apply { + putLong(NappletIpc.KEY_REQUEST_ID, id) + putString(NappletIpc.KEY_BROWSER_ORIGIN, origin) + } + } + queueToBroker(msg) + } + + private fun rememberSitePermission( + origin: String, + permission: BrowserSitePermission, + decision: Decision, + ) { + val msg = + Message.obtain(null, NappletIpc.MSG_SET_SITE_PERMISSION).apply { + data = + Bundle().apply { + putString(NappletIpc.KEY_BROWSER_ORIGIN, origin) + putString(NappletIpc.KEY_SITE_PERMISSION, permission.key) + putString(NappletIpc.KEY_SITE_DECISION, decision.name) + } + } + queueToBroker(msg) + } + + /** + * Chrome's permission bubble: "<host> wants to — Use your camera — Block / Allow". [answer] gets true + * (allow), false (block), or null when the prompt went away unanswered. + */ + private fun showPermissionPrompt( + origin: String, + permissions: List<BrowserSitePermission>, + answer: (Boolean?) -> Unit, + ) { + if (isFinishing || isDestroyed || permissionPrompt != null) { + answer(null) + return + } + var answered = false + val lines = + permissions.joinToString("\n") { + "• " + + getString( + when (it) { + BrowserSitePermission.CAMERA -> CommonsR.string.browser_permission_camera + BrowserSitePermission.MICROPHONE -> CommonsR.string.browser_permission_microphone + BrowserSitePermission.LOCATION -> CommonsR.string.browser_permission_location + }, + ) + } + permissionPrompt = + AlertDialog + .Builder(this) + .setTitle(getString(CommonsR.string.browser_permission_title, BrowserChrome.displayHost(origin))) + .setMessage(lines) + .setPositiveButton(CommonsR.string.browser_permission_allow) { _, _ -> + answered = true + answer(true) + }.setNegativeButton(CommonsR.string.browser_permission_block) { _, _ -> + answered = true + answer(false) + }.setOnDismissListener { + permissionPrompt = null + // Dismissed without choosing (back, the page cancelling): deny this time, remember nothing. + if (!answered) { + answered = true + answer(null) + } + }.show() + } + + /** Requests Android's runtime permission for each allowed site permission that lacks it. */ + private fun ensureRuntimePermissions( + allowed: Set<BrowserSitePermission>, + done: (Set<BrowserSitePermission>) -> Unit, + ) { + val needed = allowed.associateWith(::androidPermissionFor) + val missing = needed.values.filter { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED }.distinct() + if (missing.isEmpty()) return done(allowed) + if (pendingRuntimeGrant != null) return done(allowed - needed.filterValues { it in missing }.keys) + pendingRuntimeGrant = { result -> + val granted = allowed.filter { ContextCompat.checkSelfPermission(this, needed.getValue(it)) == PackageManager.PERMISSION_GRANTED }.toSet() + if (granted.size < allowed.size) Toast.makeText(this, CommonsR.string.browser_permission_system_denied, Toast.LENGTH_LONG).show() + done(granted) + } + runtimePermissionLauncher.launch(missing.toTypedArray()) + } + + private fun androidPermissionFor(permission: BrowserSitePermission): String = + when (permission) { + BrowserSitePermission.CAMERA -> Manifest.permission.CAMERA + BrowserSitePermission.MICROPHONE -> Manifest.permission.RECORD_AUDIO + BrowserSitePermission.LOCATION -> Manifest.permission.ACCESS_COARSE_LOCATION + } + + // ---- fullscreen video ---- + + private fun enterFullscreen( + view: View, + callback: WebChromeClient.CustomViewCallback, + ) { + if (customView != null) { + callback.onCustomViewHidden() + return + } + customView = view + customViewCallback = callback + view.setBackgroundColor(Color.BLACK) + // Over the whole window, outside the inset root, so the video really covers the screen. + (window.decorView as? FrameLayout)?.addView(view, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)) + WindowCompat.getInsetsController(window, window.decorView).apply { + systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + hide(WindowInsetsCompat.Type.systemBars()) + } + syncBackState() + } + + private fun exitFullscreen() { + val view = customView ?: return + customView = null + (window.decorView as? FrameLayout)?.removeView(view) + WindowCompat.getInsetsController(window, window.decorView).show(WindowInsetsCompat.Type.systemBars()) + val callback = customViewCallback + customViewCallback = null + callback?.onCustomViewHidden() + syncBackState() + } + + // ---- theme colour + Recents ---- + + /** + * Tints the system-bar areas with the page's `theme-color` (the root's padding shows through behind + * the transparent bars), and flips the bar icons to stay legible on it. Null restores the app theme. + */ + private fun applyThemeColor(color: Int?) { + val root = root ?: return + val background = color ?: resolveThemeColor(android.R.attr.colorBackground) + root.setBackgroundColor(background) + val light = ColorUtils.calculateLuminance(background) > 0.5 + WindowCompat.getInsetsController(window, root).apply { + isAppearanceLightStatusBars = light + isAppearanceLightNavigationBars = light + } + } + + /** Makes this task look like an installed app in Recents: the site's title, icon and colour. */ + @Suppress("DEPRECATION") + private fun updateTaskDescription() { + val label = pageTitle ?: title.ifBlank { null } ?: BrowserChrome.displayHost(currentUrl()) + val color = themeColor ?: 0 + val description = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ActivityManager.TaskDescription + .Builder() + .setLabel(label) + .apply { pageIcon?.let { setIcon(Icon.createWithBitmap(it)) } } + .apply { if (color != 0) setPrimaryColor(color) } + .build() + } else { + ActivityManager.TaskDescription(label, pageIcon, color) + } + runCatching { setTaskDescription(description) } + } + + // ---- long-press menu ---- + + override fun onCreateContextMenu( + menu: ContextMenu, + v: View, + menuInfo: ContextMenu.ContextMenuInfo?, + ) { + super.onCreateContextMenu(menu, v, menuInfo) + val wv = v as? WebView ?: return + val hit = wv.hitTestResult + val extra = hit.extra?.takeIf { it.isNotBlank() } ?: return + when (hit.type) { + WebView.HitTestResult.SRC_ANCHOR_TYPE -> addLinkItems(menu, extra) + WebView.HitTestResult.IMAGE_TYPE -> addImageItems(menu, extra) + WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE -> { + // An image inside a link: the hit gives the image; ask the page for the link's href. + val href = Handler(Looper.getMainLooper()).obtainMessage() + wv.requestFocusNodeHref(href) + href.data + .getString("url") + ?.takeIf { it.startsWith("http") } + ?.let { addLinkItems(menu, it) } + addImageItems(menu, extra) + } + } + } + + private fun addLinkItems( + menu: ContextMenu, + link: String, + ) { + if (menu.size() == 0) menu.setHeaderTitle(link) + if (link.startsWith("http")) { + menu.add(CommonsR.string.browser_ctx_open_new_window).setOnMenuItemClickListener { + startActivity(newWindowIntent(link)) + true + } + } + menu.add(CommonsR.string.browser_ctx_copy_link).setOnMenuItemClickListener { + BrowserWebTools.copyToClipboard(this, link) + true + } + menu.add(CommonsR.string.browser_ctx_share_link).setOnMenuItemClickListener { + BrowserWebTools.share(this, null, null, link) + true + } + } + + private fun addImageItems( + menu: ContextMenu, + image: String, + ) { + if (menu.size() == 0) menu.setHeaderTitle(image.take(80)) + if (image.startsWith("http") || image.startsWith("data:")) { + menu.add(CommonsR.string.browser_ctx_download_image).setOnMenuItemClickListener { + val wv = webView + val cookie = wv?.let { BrowserWebTools.cookieManager(it).getCookie(image) } + BrowserDownloads.download(this, image, wv?.settings?.userAgentString, null, null, cookie, if (useTor) proxyPort else -1) + true + } + } + if (image.startsWith("http")) { + menu.add(CommonsR.string.browser_ctx_copy_image_link).setOnMenuItemClickListener { + BrowserWebTools.copyToClipboard(this, image) + true + } + } + } + + /** A plain new browser window for [url], sharing this one's route, theme and account storage. */ + private fun newWindowIntent(url: String): Intent = intent(this, url, proxyPort, useTor, theme = themeType, webViewProfile = webViewProfile).addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) + + // ---- network ---- + /** * Routes WebView traffic through the Tor SOCKS proxy when [port] > 0, else clears the override. * Process-global (this `:napplet` process hosts only sandbox WebViews) and best-effort. @@ -786,11 +1282,11 @@ class NappletBrowserActivity : ComponentActivity() { private fun setNetworkMode(newUseTor: Boolean) { useTor = newUseTor applyWebViewProxy(if (useTor) proxyPort else -1) - if (this::webView.isInitialized) webView.reload() + webView?.reload() + controlSheet?.setTor(useTor) // Key the persisted choice on the host actually displayed (which may differ from startUrl after // in-page navigation), so the preference sticks to the right site. - val liveUrl = if (this::webView.isInitialized) webView.url ?: startUrl else startUrl - val host = runCatching { liveUrl.toUri().host }.getOrNull()?.takeIf { it.isNotBlank() } ?: return + val host = runCatching { currentUrl().toUri().host }.getOrNull()?.takeIf { it.isNotBlank() } ?: return val msg = Message.obtain(null, NappletIpc.MSG_SET_WEB_TOR).apply { data = @@ -809,35 +1305,115 @@ class NappletBrowserActivity : ComponentActivity() { private fun barTitle(): String = title.ifBlank { runCatching { startUrl.toUri().host }.getOrNull() ?: getString(CommonsR.string.napplet_untitled) } /** - * The top pull-down sheet: a small grabber at the top edge (out of the corner where a site shows its - * own avatar) that expands to the Tor toggle (when Tor is available) and reload. The page can't draw - * over it. Mirrors the embedded tabs' Compose `TopControlSheet`. + * The top pull-down pill: a small grabber at the top edge (out of the corner where a site shows its + * own avatar) that expands to the Chrome-PWA-style menu laid out by [BrowserChrome]. */ private fun buildControlSheet(): View = NappletControlSheet( context = this, + initialState = + BrowserChrome.State( + surface = BrowserChrome.Surface.WEB, + presentation = BrowserChrome.Presentation.FULL_SCREEN, + url = startUrl, + startUrl = startUrl, + torOn = if (proxyPort > 0) useTor else null, + ), title = barTitle(), - isSandbox = false, - onReload = { if (this::webView.isInitialized) webView.reload() }, - torInitiallyOn = if (proxyPort > 0) useTor else null, - onToggleTor = { setNetworkMode(it) }, - onInfo = null, - onPermissions = { openPermissions() }, - liveUrl = startUrl, - onNavigate = { loadAddress(it) }, - onConsole = { show -> consolePanel?.setShowing(show) }, + listener = sheetListener, isFavoriteInitially = intent.getBooleanExtra(EXTRA_IS_FAVORITE, false), - onFavoriteToggle = { url, isFavorite -> sendFavoriteToggle(url, isFavorite) }, ).also { controlSheet = it } + private val sheetListener = + object : NappletControlSheet.Listener { + override fun onAction(action: Action) { + val wv = webView + when (action) { + Action.BACK -> wv?.goBack() + Action.FORWARD -> wv?.goForward() + Action.RELOAD -> wv?.reload() + Action.STOP -> wv?.stopLoading() + Action.FAVORITE -> sendFavoriteToggle(currentUrl(), controlSheet?.wantsFavorite() ?: true) + Action.SHARE -> BrowserWebTools.share(this@NappletBrowserActivity, pageTitle, null, currentUrl()) + Action.BACK_TO_APP -> wv?.let { BrowserWebTools.backToScope(it, startUrl) } + Action.COPY_LINK -> BrowserWebTools.copyToClipboard(this@NappletBrowserActivity, currentUrl()) + Action.EDIT_ADDRESS -> Unit + Action.FIND_IN_PAGE -> { + setConsoleShowing(false) + findBar?.show() + syncBackState() + } + Action.TEXT_SIZE -> Unit + Action.DESKTOP_SITE -> + wv?.let { + val desktop = !BrowserWebTools.isDesktopMode(it) + BrowserWebTools.setDesktopMode(it, desktop) + controlSheet?.setDesktopSite(desktop) + } + Action.ADD_TO_HOME_SCREEN -> addToHomeScreen() + Action.OPEN_IN_BROWSER_APP -> BrowserWebTools.openInOtherBrowser(this@NappletBrowserActivity, currentUrl()) + Action.OPEN_FULL_SCREEN -> Unit + Action.TOR -> setNetworkMode(!useTor) + Action.ACCESS_INFO -> Unit + Action.SITE_SETTINGS -> openPermissions() + Action.CONSOLE -> setConsoleShowing(!consoleShowing) + } + } + + override fun onNavigate(text: String) = loadAddress(text) + + override fun onTextZoom(percent: Int) { + webView?.let { BrowserWebTools.setTextZoom(it, percent) } + } + + override fun onOriginTap() = showPageInfo() + + override fun onClose() = finish() + } + + private fun setConsoleShowing(showing: Boolean) { + consoleShowing = showing + if (showing) findBar?.hide() + consolePanel?.setShowing(showing) + controlSheet?.setConsoleShowing(showing) + } + + /** Chrome's page-info sheet: connection, Tor, certificate, then site settings and clearing its data. */ + private fun showPageInfo() { + val wv = webView ?: return + val url = currentUrl() + AlertDialog + .Builder(this) + .setTitle(BrowserChrome.displayHost(url)) + .setMessage(BrowserWebTools.pageInfo(this, wv, if (proxyPort > 0) useTor else null)) + .setPositiveButton(android.R.string.ok, null) + .setNeutralButton(CommonsR.string.browser_page_info_permissions) { _, _ -> openPermissions() } + .setNegativeButton(CommonsR.string.browser_page_info_clear_data) { _, _ -> + webView?.let { BrowserWebTools.clearSiteData(this, it, url) } + }.show() + } + + /** Asks the main process to pin a launcher shortcut that reopens this page in Amethyst's browser. */ + private fun addToHomeScreen() { + val url = currentUrl() + val msg = + Message.obtain(null, NappletIpc.MSG_ADD_TO_HOME_SCREEN).apply { + data = + Bundle().apply { + putString(NappletIpc.KEY_FAVORITE_URL, url) + putString(NappletIpc.KEY_FAVORITE_LABEL, pageTitle ?: BrowserChrome.displayHost(url)) + } + } + queueToBroker(msg) + } + /** * Ask the broker to open the editable permission screen for the site currently displayed. NIP-07 grants * for a plain browser are keyed per visited origin (`browser:<origin>`), so we send the live origin and * the broker launches the main activity at that Connected Apps detail. */ private fun openPermissions() { - val liveUrl = if (this::webView.isInitialized) webView.url ?: startUrl else startUrl - val uri = runCatching { liveUrl.toUri() }.getOrNull() ?: return + val uri = runCatching { currentUrl().toUri() }.getOrNull() ?: return val scheme = uri.scheme?.takeIf { it.isNotBlank() } ?: return val host = uri.host?.takeIf { it.isNotBlank() } ?: return val origin = "$scheme://$host" + if (uri.port > 0) ":${uri.port}" else "" @@ -845,7 +1421,7 @@ class NappletBrowserActivity : ComponentActivity() { Message.obtain(null, NappletIpc.MSG_OPEN_PERMISSIONS).apply { data = Bundle().apply { putString(NappletIpc.KEY_BROWSER_ORIGIN, origin) } } - if (brokerMessenger != null) sendToBroker(msg) else pendingBrokerRequests.add(msg) + queueToBroker(msg) } /** @@ -856,18 +1432,18 @@ class NappletBrowserActivity : ComponentActivity() { url: String, isFavorite: Boolean, ) { - val host = runCatching { url.toUri().host }.getOrNull()?.takeIf { it.isNotBlank() } ?: url + val label = pageTitle ?: runCatching { url.toUri().host }.getOrNull()?.takeIf { it.isNotBlank() } ?: url val msg = Message.obtain(null, NappletIpc.MSG_TOGGLE_WEB_FAVORITE).apply { replyTo = replyMessenger data = Bundle().apply { putString(NappletIpc.KEY_FAVORITE_URL, url) - putString(NappletIpc.KEY_FAVORITE_LABEL, host) + putString(NappletIpc.KEY_FAVORITE_LABEL, label) putBoolean(NappletIpc.KEY_FAVORITE_IS_FAVORITE, isFavorite) } } - if (brokerMessenger != null) sendToBroker(msg) else pendingBrokerRequests.add(msg) + queueToBroker(msg) } private fun buildConsolePanel(): View = @@ -894,6 +1470,40 @@ class NappletBrowserActivity : ComponentActivity() { addView(ProgressBar(this@NappletBrowserActivity)) } + /** Chrome's "Aw, Snap!": the page's renderer died; offer to load [url] again in a fresh WebView. */ + private fun showCrashView(url: String) { + crashView?.let { contentFrame.removeView(it) } + crashView = + LinearLayout(this) + .apply { + orientation = LinearLayout.VERTICAL + gravity = Gravity.CENTER + setBackgroundColor(resolveThemeColor(android.R.attr.colorBackground)) + layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT) + addView( + TextView(this@NappletBrowserActivity).apply { + text = getString(CommonsR.string.browser_renderer_gone) + setTextColor(resolveThemeColor(android.R.attr.textColorPrimary)) + textSize = 18f + gravity = Gravity.CENTER + }, + ) + addView( + Button(this@NappletBrowserActivity).apply { + text = getString(CommonsR.string.browser_renderer_gone_reload) + setOnClickListener { + crashView?.let { contentFrame.removeView(it) } + crashView = null + val wv = buildWebView() + contentFrame.addView(wv, 0, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)) + wv.loadUrl(url) + } + }, + ) + }.also { contentFrame.addView(it) } + syncBackState() + } + /** * A thin determinate progress bar pinned to the top edge, like a browser's. Driven by * [BrowserChromeClient.onProgressChanged]: visible while the page loads and gone at 100%. @@ -911,6 +1521,7 @@ class NappletBrowserActivity : ComponentActivity() { private fun updateLoadProgress(progress: Int) { if (progress >= 100) { topProgressBar.visibility = View.GONE + controlSheet?.setLoading(false) } else { topProgressBar.progress = progress topProgressBar.visibility = View.VISIBLE @@ -928,7 +1539,7 @@ class NappletBrowserActivity : ComponentActivity() { } private fun resolveThemeColor(attr: Int): Int { - val tv = android.util.TypedValue() + val tv = TypedValue() theme.resolveAttribute(attr, tv, true) return if (tv.resourceId != 0) ContextCompat.getColor(this, tv.resourceId) else tv.data } @@ -939,6 +1550,7 @@ class NappletBrowserActivity : ComponentActivity() { companion object { private const val TAG = "NappletBrowserActivity" + private const val ACTIVITY_CLASS = "com.vitorpamplona.amethyst.napplethost.NappletBrowserActivity" /** How often a resumed browser renews its foreground lease (well under the broker's 90s TTL). */ private const val FOREGROUND_HEARTBEAT_MS = 30_000L @@ -955,6 +1567,7 @@ class NappletBrowserActivity : ComponentActivity() { private const val EXTRA_TITLE = "title" private const val EXTRA_THEME = "theme" private const val EXTRA_IS_FAVORITE = "isFavorite" + private const val EXTRA_POPUP_TOKEN = "popupToken" fun intent( context: Context, @@ -967,7 +1580,7 @@ class NappletBrowserActivity : ComponentActivity() { webViewProfile: String? = null, ): Intent = Intent() - .setClassName(context, "com.vitorpamplona.amethyst.napplethost.NappletBrowserActivity") + .setClassName(context, ACTIVITY_CLASS) .putExtra(EXTRA_URL, url) .putExtra(EXTRA_PROXY_PORT, proxyPort) .putExtra(EXTRA_USE_TOR, useTor) @@ -979,5 +1592,19 @@ class NappletBrowserActivity : ComponentActivity() { .putExtra(NappletHostContract.EXTRA_WEBVIEW_PROFILE, webViewProfile) // Distinct task identity per URL for documentLaunchMode=intoExisting. .setData(url.toUri()) + + /** + * Opens, as its own task, a window a page asked for; [token] names the popup WebView parked in + * [BrowserPopups]. Usable from the embedded browser's Service as well as from an Activity. + */ + fun popupIntent( + context: Context, + token: String, + ): Intent = + Intent() + .setClassName(context, ACTIVITY_CLASS) + .putExtra(EXTRA_POPUP_TOKEN, token) + .setData("amethyst-window://$token".toUri()) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) } } diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserContract.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserContract.kt index c4d2ba760a..01015fc67a 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserContract.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserContract.kt @@ -116,6 +116,91 @@ object NappletBrowserContract { */ const val MSG_FILE_CHOOSER_RESULT = 15 + // ---- PWA-parity controls (see BrowserChrome). Client → provider unless noted. ---- + + /** Go forward in the page history. */ + const val MSG_FORWARD = 16 + + /** Stop the current load. */ + const val MSG_STOP = 17 + + /** Find [KEY_FIND_QUERY] in the page (empty clears). Provider answers with [MSG_FIND_RESULT]. */ + const val MSG_FIND = 18 + + /** Move to the next ([KEY_FIND_FORWARD] = true) or previous match. */ + const val MSG_FIND_NEXT = 19 + + /** Provider → client: [KEY_FIND_ACTIVE] (0-based) of [KEY_FIND_TOTAL] matches. */ + const val MSG_FIND_RESULT = 20 + + /** Switch desktop-site mode ([KEY_ENABLED]); the page reloads. */ + const val MSG_SET_DESKTOP = 21 + + /** Set the text size to [KEY_TEXT_ZOOM] percent. */ + const val MSG_SET_TEXT_ZOOM = 22 + + /** Step back to the most recent page on [KEY_URL]'s origin (the app's home), or load it. */ + const val MSG_BACK_TO_SCOPE = 23 + + /** Clear the current site's cookies and storage in this account's profile, then reload. */ + const val MSG_CLEAR_SITE_DATA = 24 + + /** Ask for the page-info text (connection, Tor, certificate); answered with [MSG_PAGE_INFO]. */ + const val MSG_PAGE_INFO_REQUEST = 25 + + /** Provider → client: [KEY_PAGE_INFO] for the page on screen. */ + const val MSG_PAGE_INFO = 26 + + /** + * Provider → client: the page opened a JS dialog. [KEY_DIALOG_ID], [KEY_DIALOG_TYPE] (`alert`, `confirm`, + * `prompt`, `beforeunload`), [KEY_URL], [KEY_DIALOG_MESSAGE], [KEY_DIALOG_DEFAULT], and + * [KEY_DIALOG_OFFER_BLOCK] when "Block dialogs from this page" should be offered. The page's JS waits + * until [MSG_JS_DIALOG_RESULT] arrives. + */ + const val MSG_JS_DIALOG = 27 + + /** The user's answer: [KEY_DIALOG_ID], [KEY_DIALOG_CONFIRMED], [KEY_DIALOG_TEXT], [KEY_DIALOG_BLOCK]. */ + const val MSG_JS_DIALOG_RESULT = 28 + + /** + * Provider → client: the page asked for camera / microphone / location. [KEY_PERMISSION_ID], + * [KEY_BROWSER_ORIGIN], [KEY_PERMISSIONS] (`BrowserSitePermission` keys). Answered with + * [MSG_PERMISSION_RESULT]. + */ + const val MSG_PERMISSION_REQUEST = 29 + + /** The granted subset: [KEY_PERMISSION_ID], [KEY_PERMISSIONS]. */ + const val MSG_PERMISSION_RESULT = 30 + + /** Provider → client: the page withdrew request [KEY_PERMISSION_ID]; drop its prompt. */ + const val MSG_PERMISSION_CANCEL = 31 + + /** Provider → client: HTML fullscreen entered or left ([KEY_ENABLED]). */ + const val MSG_FULLSCREEN = 32 + + /** Leave HTML fullscreen (the user pressed back). */ + const val MSG_EXIT_FULLSCREEN = 33 + + const val KEY_CAN_GO_FORWARD = "canGoForward" + const val KEY_FIND_QUERY = "findQuery" + const val KEY_FIND_FORWARD = "findForward" + const val KEY_FIND_ACTIVE = "findActive" + const val KEY_FIND_TOTAL = "findTotal" + const val KEY_ENABLED = "enabled" + const val KEY_TEXT_ZOOM = "textZoom" + const val KEY_PAGE_INFO = "pageInfo" + const val KEY_DIALOG_ID = "dialogId" + const val KEY_DIALOG_TYPE = "dialogType" + const val KEY_DIALOG_MESSAGE = "dialogMessage" + const val KEY_DIALOG_DEFAULT = "dialogDefault" + const val KEY_DIALOG_OFFER_BLOCK = "dialogOfferBlock" + const val KEY_DIALOG_CONFIRMED = "dialogConfirmed" + const val KEY_DIALOG_TEXT = "dialogText" + const val KEY_DIALOG_BLOCK = "dialogBlock" + const val KEY_PERMISSION_ID = "permissionId" + const val KEY_PERMISSIONS = "permissions" + const val KEY_BROWSER_ORIGIN = "browserOrigin" + const val KEY_FILE_CHOOSER_ID = "fileChooserId" const val KEY_FILE_CHOOSER_ACCEPT = "fileChooserAccept" const val KEY_FILE_CHOOSER_MULTIPLE = "fileChooserMultiple" diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserService.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserService.kt index 665414d757..aa5822da95 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserService.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserService.kt @@ -27,6 +27,7 @@ import android.content.Intent import android.content.ServiceConnection import android.graphics.Bitmap import android.graphics.Canvas +import android.graphics.Color import android.net.Uri import android.os.Build import android.os.Bundle @@ -36,23 +37,34 @@ import android.os.Looper import android.os.Message import android.os.Messenger import android.os.SystemClock +import android.view.View +import android.view.ViewGroup import android.webkit.ConsoleMessage +import android.webkit.GeolocationPermissions +import android.webkit.JsPromptResult +import android.webkit.JsResult +import android.webkit.PermissionRequest +import android.webkit.RenderProcessGoneDetail import android.webkit.ValueCallback import android.webkit.WebChromeClient import android.webkit.WebResourceError import android.webkit.WebResourceRequest -import android.webkit.WebSettings import android.webkit.WebView import android.webkit.WebViewClient +import android.widget.FrameLayout import androidx.annotation.RequiresApi import androidx.core.graphics.createBitmap import androidx.core.graphics.scale import androidx.core.net.toUri import androidx.privacysandbox.ui.provider.toCoreLibInfo import androidx.webkit.JavaScriptReplyProxy +import androidx.webkit.ProxyConfig +import androidx.webkit.ProxyController import androidx.webkit.WebMessageCompat import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission import com.vitorpamplona.amethyst.commons.browser.OmniboxInput import com.vitorpamplona.amethyst.commons.napplet.NappletWebContract import com.vitorpamplona.amethyst.commons.util.parseJsonObjectOrNull @@ -61,6 +73,7 @@ import com.vitorpamplona.amethyst.commons.util.withString import com.vitorpamplona.quartz.utils.Log import kotlinx.serialization.json.JsonObject import java.io.ByteArrayOutputStream +import java.util.concurrent.Executor /** * Provider for the **embedded** in-app browser. Runs in the keyless `:napplet` process: it hosts the @@ -97,6 +110,20 @@ class NappletBrowserService : Service() { val webViewProfile: String?, ) { var webView: WebView? = null + + // The session's root view (holds the WebView, and the page's fullscreen view when it has one). + var container: FrameLayout? = null + var customView: View? = null + var customViewCallback: WebChromeClient.CustomViewCallback? = null + + // Page-originated JS dialogs and permission requests waiting on the main process's answer. + val jsDialogs = mutableMapOf<Long, JsResult>() + var jsDialogsOnPage = 0 + var jsDialogsBlocked = false + val permissionRequests = mutableMapOf<Long, (Set<BrowserSitePermission>) -> Unit>() + var textZoom = BrowserChrome.DEFAULT_TEXT_ZOOM + var desktopSite = false + var bridgeReplyProxy: JavaScriptReplyProxy? = null var fireSeq = 0 @@ -124,6 +151,9 @@ class NappletBrowserService : Service() { private val tabs = mutableMapOf<String, BrowserTab>() + // WebView's PermissionRequest → our relay id, so a page's cancellation can withdraw the prompt. + private val pendingWebPermissions = mutableMapOf<PermissionRequest, Long>() + // The shim never changes; read+decode it once instead of per tab on the main thread. private val shimJs: String by lazy { readContractAsset(NappletWebContract.SHIM_JS_PATH).decodeToString() } @@ -157,6 +187,7 @@ class NappletBrowserService : Service() { } tabs.values.forEach { it.fileChooser.cancel() + cancelPending(it) it.webView?.destroy() } tabs.clear() @@ -177,7 +208,7 @@ class NappletBrowserService : Service() { url = data.getString(NappletBrowserContract.KEY_URL)?.ifBlank { ABOUT_BLANK } ?: ABOUT_BLANK, proxyPort = data.getInt(NappletBrowserContract.KEY_PROXY_PORT, -1), useTor = data.getBoolean(NappletBrowserContract.KEY_USE_TOR, false), - bgColor = data.getInt(NappletBrowserContract.KEY_BG_COLOR, android.graphics.Color.WHITE), + bgColor = data.getInt(NappletBrowserContract.KEY_BG_COLOR, Color.WHITE), themeType = data.getString(NappletBrowserContract.KEY_THEME).orEmpty().ifBlank { "SYSTEM" }, webViewProfile = data.getString(NappletBrowserContract.KEY_WEBVIEW_PROFILE), ) @@ -189,7 +220,77 @@ class NappletBrowserService : Service() { } replyWithAdapter(tab) } - NappletBrowserContract.MSG_NAVIGATE -> tabFor(msg)?.webView?.loadUrl(normalizeUrl(msg.data?.getString(NappletBrowserContract.KEY_URL).orEmpty())) + NappletBrowserContract.MSG_NAVIGATE -> { + val tab = tabFor(msg) ?: return true + val url = normalizeUrl(msg.data?.getString(NappletBrowserContract.KEY_URL).orEmpty()) + // A renderer crash destroyed this tab's WebView; the user's retry builds a fresh one. + if (tab.webView == null) rebuildWebView(tab, url) else tab.webView?.loadUrl(url) + } + NappletBrowserContract.MSG_FORWARD -> tabFor(msg)?.webView?.let { if (it.canGoForward()) it.goForward() } + NappletBrowserContract.MSG_STOP -> tabFor(msg)?.webView?.stopLoading() + NappletBrowserContract.MSG_FIND -> { + val tab = tabFor(msg) ?: return true + val wv = tab.webView ?: return true + val query = msg.data?.getString(NappletBrowserContract.KEY_FIND_QUERY).orEmpty() + if (query.isEmpty()) { + wv.clearMatches() + wv.setFindListener(null) + } else { + wv.setFindListener { active, total, _ -> pushFindResult(tab, active, total) } + wv.findAllAsync(query) + } + } + NappletBrowserContract.MSG_FIND_NEXT -> tabFor(msg)?.webView?.findNext(msg.data?.getBoolean(NappletBrowserContract.KEY_FIND_FORWARD, true) ?: true) + NappletBrowserContract.MSG_SET_DESKTOP -> { + val tab = tabFor(msg) ?: return true + tab.desktopSite = msg.data?.getBoolean(NappletBrowserContract.KEY_ENABLED, false) ?: false + tab.webView?.let { BrowserWebTools.setDesktopMode(it, tab.desktopSite) } + } + NappletBrowserContract.MSG_SET_TEXT_ZOOM -> { + val tab = tabFor(msg) ?: return true + tab.textZoom = msg.data?.getInt(NappletBrowserContract.KEY_TEXT_ZOOM, BrowserChrome.DEFAULT_TEXT_ZOOM) ?: BrowserChrome.DEFAULT_TEXT_ZOOM + tab.webView?.let { BrowserWebTools.setTextZoom(it, tab.textZoom) } + } + NappletBrowserContract.MSG_BACK_TO_SCOPE -> { + val tab = tabFor(msg) ?: return true + tab.webView?.let { BrowserWebTools.backToScope(it, msg.data?.getString(NappletBrowserContract.KEY_URL) ?: tab.url) } + } + NappletBrowserContract.MSG_CLEAR_SITE_DATA -> { + val tab = tabFor(msg) ?: return true + tab.webView?.let { wv -> wv.url?.let { BrowserWebTools.clearSiteData(this, wv, it) } } + } + NappletBrowserContract.MSG_PAGE_INFO_REQUEST -> { + val tab = tabFor(msg) ?: return true + val wv = tab.webView ?: return true + sendToClient(tab, NappletBrowserContract.MSG_PAGE_INFO) { + putString(NappletBrowserContract.KEY_PAGE_INFO, BrowserWebTools.pageInfo(this@NappletBrowserService, wv, if (tab.proxyPort > 0) tab.useTor else null)) + } + } + NappletBrowserContract.MSG_JS_DIALOG_RESULT -> { + val tab = tabFor(msg) ?: return true + val data = msg.data ?: return true + val result = tab.jsDialogs.remove(data.getLong(NappletBrowserContract.KEY_DIALOG_ID)) ?: return true + if (data.getBoolean(NappletBrowserContract.KEY_DIALOG_BLOCK, false)) tab.jsDialogsBlocked = true + val confirmed = data.getBoolean(NappletBrowserContract.KEY_DIALOG_CONFIRMED, false) + when { + !confirmed -> result.cancel() + result is JsPromptResult -> result.confirm(data.getString(NappletBrowserContract.KEY_DIALOG_TEXT).orEmpty()) + else -> result.confirm() + } + } + NappletBrowserContract.MSG_PERMISSION_RESULT -> { + val tab = tabFor(msg) ?: return true + val data = msg.data ?: return true + val answer = tab.permissionRequests.remove(data.getLong(NappletBrowserContract.KEY_PERMISSION_ID)) ?: return true + answer( + data + .getStringArray(NappletBrowserContract.KEY_PERMISSIONS) + .orEmpty() + .mapNotNull(BrowserSitePermission::fromKey) + .toSet(), + ) + } + NappletBrowserContract.MSG_EXIT_FULLSCREEN -> tabFor(msg)?.let { exitFullscreen(it) } NappletBrowserContract.MSG_RELOAD -> tabFor(msg)?.webView?.reload() NappletBrowserContract.MSG_BACK -> tabFor(msg)?.webView?.let { if (it.canGoBack()) it.goBack() } NappletBrowserContract.MSG_IME_OP -> { @@ -285,10 +386,20 @@ class NappletBrowserService : Service() { fun createBrowserWebView( context: Context, sessionId: String, + container: FrameLayout, ): WebView { // The session may have been closed between MSG_CREATE_SESSION and this posted call — fail rather // than build a WebView that no tab tracks (it would leak). val tab = tabs[sessionId] ?: error("No browser tab for session $sessionId") + tab.container = container + return buildTabWebView(context, tab).also { it.loadUrl(tab.url) } + } + + /** Builds [tab]'s WebView with every client, bridge and script wired, without loading anything. */ + private fun buildTabWebView( + context: Context, + tab: BrowserTab, + ): WebView { val wv = WebView(nightThemedContext(context, tab.themeType)) // FIRST touch after construction: setProfile throws once the WebView has loaded content (or its // profile has otherwise been used), so the storage partition must be chosen before the @@ -302,58 +413,182 @@ class NappletBrowserService : Service() { WebViewCompat.addWebMessageListener(wv, NappletWebContract.BRIDGE_NAME, setOf("*")) { view, message, sourceOrigin, isMainFrame, replyProxy -> onBridgeMessage(tab, view, message, sourceOrigin, isMainFrame, replyProxy) } - // __nappletImeProxy: this is the EMBEDDED surface (no native keyboard), so install the IME agent - // that relays the focused field to the host's keyboard. The full-screen browser activity sets the - // direct bridge but NOT this flag (it has a real WebView window with a native keyboard). - val startScript = "if (window.top === window) { window.__nappletDirectBridge = true; window.__nappletNip07 = true; window.__nappletImeProxy = true; }\n$shimJs" - WebViewCompat.addDocumentStartJavaScript(wv, startScript, setOf("*")) + // imeProxy: this is the EMBEDDED surface (no native keyboard), so install the IME agent that relays + // the focused field to the host's keyboard. The full-screen browser activity sets the direct bridge + // but NOT this flag (it has a real WebView window with a native keyboard). + WebViewCompat.addDocumentStartJavaScript(wv, BrowserWebTools.browserStartScript(shimJs, imeProxy = true), setOf("*")) + BrowserWebTools.setTextZoom(wv, tab.textZoom) + if (tab.desktopSite) BrowserWebTools.setDesktopMode(wv, true) tab.webView = wv - wv.loadUrl(tab.url) return wv } + /** After a renderer crash: a fresh WebView in the same surface, loading [url]. */ + private fun rebuildWebView( + tab: BrowserTab, + url: String, + ) { + val container = tab.container ?: return + val wv = buildTabWebView(container.context, tab) + container.addView(wv, 0, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)) + wv.loadUrl(url) + } + /** A session closed: drop the tab and destroy its own WebView (never a sibling's). */ fun onSessionClosed(sessionId: String) { val tab = tabs.remove(sessionId) ?: return tab.bridgeReplyProxy = null // Release a picker still waiting on this surface before its WebView goes away. tab.fileChooser.cancel() + cancelPending(tab) + tab.customViewCallback?.onCustomViewHidden() + tab.customViewCallback = null + tab.customView = null + tab.container = null tab.webView?.destroy() tab.webView = null } - @Suppress("SetJavaScriptEnabled") private fun configureWebView( wv: WebView, tab: BrowserTab?, ) { - wv.settings.apply { - javaScriptEnabled = true - domStorageEnabled = true - @Suppress("DEPRECATION") - databaseEnabled = false - allowFileAccess = false - allowContentAccess = false - @Suppress("DEPRECATION") - allowFileAccessFromFileURLs = false - @Suppress("DEPRECATION") - allowUniversalAccessFromFileURLs = false - javaScriptCanOpenWindowsAutomatically = false - setSupportMultipleWindows(false) - setGeolocationEnabled(false) - mediaPlaybackRequiresUserGesture = true - builtInZoomControls = true - displayZoomControls = false - loadWithOverviewMode = true - useWideViewPort = true - mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE - if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLE)) { - safeBrowsingEnabled = true - } - } - WebView.setWebContentsDebuggingEnabled(false) + BrowserWebTools.applyBrowserSettings(wv) wv.webViewClient = BrowserClient(tab) wv.webChromeClient = BrowserChromeClient(tab) + wv.setDownloadListener { url, userAgent, contentDisposition, mimeType, _ -> + val route = if (tab != null && tab.useTor) tab.proxyPort else -1 + BrowserDownloads.download(this, url, userAgent, contentDisposition, mimeType, BrowserWebTools.cookieManager(wv).getCookie(url), route) + } + } + + /** Answers everything [tab] still has outstanding, so no page stays blocked on a torn-down surface. */ + private fun cancelPending(tab: BrowserTab) { + tab.jsDialogs.values.forEach { it.cancel() } + tab.jsDialogs.clear() + pendingWebPermissions.values.removeAll(tab.permissionRequests.keys) + tab.permissionRequests.values.forEach { it(emptySet()) } + tab.permissionRequests.clear() + } + + private inline fun sendToClient( + tab: BrowserTab, + what: Int, + crossinline block: Bundle.() -> Unit, + ): Boolean { + val client = tab.clientMessenger ?: return false + val message = Message.obtain(null, what).apply { data = Bundle().apply(block) } + return runCatching { client.send(message) }.isSuccess + } + + private fun pushFindResult( + tab: BrowserTab, + active: Int, + total: Int, + ) { + sendToClient(tab, NappletBrowserContract.MSG_FIND_RESULT) { + putInt(NappletBrowserContract.KEY_FIND_ACTIVE, active) + putInt(NappletBrowserContract.KEY_FIND_TOTAL, total) + } + } + + private var dialogSeq = 0L + + /** + * Relays a page's JS dialog to the main process, which draws it over the tab (this provider has no + * window). Answers at once when dialogs are blocked for this page or no client can show one. + */ + private fun relayJsDialog( + tab: BrowserTab?, + type: String, + url: String?, + message: String?, + defaultValue: String?, + result: JsResult, + ): Boolean { + if (tab == null) { + result.cancel() + return true + } + if (tab.jsDialogsBlocked) { + // A blocked page may no longer hold the user on it: leaving is allowed, everything else cancels. + if (type == "beforeunload") result.confirm() else result.cancel() + return true + } + val id = ++dialogSeq + tab.jsDialogs[id] = result + tab.jsDialogsOnPage++ + val sent = + sendToClient(tab, NappletBrowserContract.MSG_JS_DIALOG) { + putLong(NappletBrowserContract.KEY_DIALOG_ID, id) + putString(NappletBrowserContract.KEY_DIALOG_TYPE, type) + putString(NappletBrowserContract.KEY_URL, url) + putString(NappletBrowserContract.KEY_DIALOG_MESSAGE, message) + putString(NappletBrowserContract.KEY_DIALOG_DEFAULT, defaultValue) + putBoolean(NappletBrowserContract.KEY_DIALOG_OFFER_BLOCK, tab.jsDialogsOnPage > 1) + } + if (!sent) tab.jsDialogs.remove(id)?.cancel() + return true + } + + private var permissionSeq = 0L + + /** Relays a camera / microphone / location request to the main process, which owns the prompt. */ + private fun relayPermissionRequest( + tab: BrowserTab?, + origin: String?, + wanted: Set<BrowserSitePermission>, + answer: (Set<BrowserSitePermission>) -> Unit, + ): Long? { + if (tab == null || origin == null || wanted.isEmpty()) { + answer(emptySet()) + return null + } + val id = ++permissionSeq + tab.permissionRequests[id] = answer + val sent = + sendToClient(tab, NappletBrowserContract.MSG_PERMISSION_REQUEST) { + putLong(NappletBrowserContract.KEY_PERMISSION_ID, id) + putString(NappletBrowserContract.KEY_BROWSER_ORIGIN, origin) + putStringArray(NappletBrowserContract.KEY_PERMISSIONS, wanted.map { it.key }.toTypedArray()) + } + if (!sent) tab.permissionRequests.remove(id)?.invoke(emptySet()) + return id + } + + private fun sitePermissionFor(resource: String): BrowserSitePermission? = + when (resource) { + PermissionRequest.RESOURCE_VIDEO_CAPTURE -> BrowserSitePermission.CAMERA + PermissionRequest.RESOURCE_AUDIO_CAPTURE -> BrowserSitePermission.MICROPHONE + else -> null + } + + /** HTML fullscreen inside the surface: the page's view covers the tab; back (from the client) leaves it. */ + private fun enterFullscreen( + tab: BrowserTab?, + view: View, + callback: WebChromeClient.CustomViewCallback, + ) { + val container = tab?.container + if (tab == null || container == null || tab.customView != null) { + callback.onCustomViewHidden() + return + } + tab.customView = view + tab.customViewCallback = callback + view.setBackgroundColor(Color.BLACK) + container.addView(view, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)) + sendToClient(tab, NappletBrowserContract.MSG_FULLSCREEN) { putBoolean(NappletBrowserContract.KEY_ENABLED, true) } + } + + private fun exitFullscreen(tab: BrowserTab) { + val view = tab.customView ?: return + tab.customView = null + tab.container?.removeView(view) + val callback = tab.customViewCallback + tab.customViewCallback = null + callback?.onCustomViewHidden() + sendToClient(tab, NappletBrowserContract.MSG_FULLSCREEN) { putBoolean(NappletBrowserContract.KEY_ENABLED, false) } } private inner class BrowserChromeClient( @@ -392,6 +627,91 @@ class NappletBrowserService : Service() { title: String?, ) = pushUrl(tab, view) + // This WebView is built from a Service context, so the framework can't show JS dialogs itself + // (it needs an Activity); the main process draws them over the tab instead. + override fun onJsAlert( + view: WebView, + url: String?, + message: String?, + result: JsResult, + ): Boolean = relayJsDialog(tab, "alert", url, message, null, result) + + override fun onJsConfirm( + view: WebView, + url: String?, + message: String?, + result: JsResult, + ): Boolean = relayJsDialog(tab, "confirm", url, message, null, result) + + override fun onJsPrompt( + view: WebView, + url: String?, + message: String?, + defaultValue: String?, + result: JsPromptResult, + ): Boolean = relayJsDialog(tab, "prompt", url, message, defaultValue, result) + + override fun onJsBeforeUnload( + view: WebView, + url: String?, + message: String?, + result: JsResult, + ): Boolean = relayJsDialog(tab, "beforeunload", url, message, null, result) + + /** `_blank` / user-initiated `window.open()`: a new full-screen browser window, `opener` intact. */ + override fun onCreateWindow( + view: WebView, + isDialog: Boolean, + isUserGesture: Boolean, + resultMsg: Message, + ): Boolean { + val tab = tab ?: return false + if (!isUserGesture) return false + val transport = resultMsg.obj as? WebView.WebViewTransport ?: return false + val (token, child) = BrowserPopups.create(this@NappletBrowserService, shimJs, tab.proxyPort, tab.useTor, tab.themeType, tab.webViewProfile) + transport.webView = child + resultMsg.sendToTarget() + runCatching { startActivity(NappletBrowserActivity.popupIntent(this@NappletBrowserService, token)) } + .onFailure { Log.w(TAG, "Could not open the new window", it) } + return true + } + + override fun onPermissionRequest(request: PermissionRequest) { + val wanted = request.resources.mapNotNull(::sitePermissionFor).toSet() + val id = + relayPermissionRequest(tab, BrowserChrome.originOf(request.origin.toString()), wanted) { granted -> + val resources = request.resources.filter { sitePermissionFor(it) in granted }.toTypedArray() + if (resources.isEmpty()) request.deny() else request.grant(resources) + } + if (id != null) pendingWebPermissions[request] = id + } + + override fun onPermissionRequestCanceled(request: PermissionRequest) { + val id = pendingWebPermissions.remove(request) ?: return + val tab = tab ?: return + tab.permissionRequests.remove(id) + sendToClient(tab, NappletBrowserContract.MSG_PERMISSION_CANCEL) { putLong(NappletBrowserContract.KEY_PERMISSION_ID, id) } + } + + override fun onGeolocationPermissionsShowPrompt( + origin: String, + callback: GeolocationPermissions.Callback, + ) { + relayPermissionRequest(tab, BrowserChrome.originOf(origin), setOf(BrowserSitePermission.LOCATION)) { granted -> + // Never let WebView remember it: the answer lives in the main-process registry. + callback.invoke(origin, BrowserSitePermission.LOCATION in granted, false) + } + } + + override fun onShowCustomView( + view: View, + callback: CustomViewCallback, + ) = enterFullscreen(tab, view, callback) + + override fun onHideCustomView() { + tab?.let { exitFullscreen(it) } + } + override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { if (tab == null) return false pushConsoleLog( @@ -468,19 +788,18 @@ class NappletBrowserService : Service() { val uri = request.url val scheme = uri.scheme?.lowercase() if (scheme == "http" || scheme == "https") return false - if (request.hasGesture()) { - runCatching { startActivity(Intent(Intent.ACTION_VIEW, uri).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) } - } - return true + return BrowserWebTools.openExternal(this@NappletBrowserService, uri, request.hasGesture()) { view.loadUrl(it) } } override fun onPageStarted( view: WebView, url: String, - favicon: android.graphics.Bitmap?, + favicon: Bitmap?, ) { - // A new main-frame navigation cleared any prior error. + // A new main-frame navigation cleared any prior error, and lifts "block this page's dialogs". tab?.loadFailed = false + tab?.jsDialogsOnPage = 0 + tab?.jsDialogsBlocked = false // Re-arm favicon capture when the host changes, so a same-host in-page nav doesn't re-send. if (tab != null && OmniboxInput.hostOf(url) != tab.lastIconHost) tab.lastIconHost = null // view.title still names the page being left; the new one's arrives via onReceivedTitle. @@ -514,6 +833,35 @@ class NappletBrowserService : Service() { tab?.loadFailed = true pushLoadState(tab, view, isLoading = false) } + + /** + * The renderer died. It is shared by every WebView in `:napplet`, and an unhandled crash kills the + * whole process — every other tab included. Drop just this tab's WebView and report the load as + * failed; the tab's retry (MSG_NAVIGATE) builds a fresh WebView in the same surface. + */ + override fun onRenderProcessGone( + view: WebView, + detail: RenderProcessGoneDetail, + ): Boolean { + Log.w(TAG) { "Renderer gone (crashed=${detail.didCrash()}) for an embedded tab" } + (view.parent as? ViewGroup)?.removeView(view) + view.destroy() + val tab = tab ?: return true + if (tab.webView === view) { + tab.webView = null + tab.customView?.let { tab.container?.removeView(it) } + tab.customView = null + tab.customViewCallback = null + cancelPending(tab) + tab.loadFailed = true + sendToClient(tab, NappletBrowserContract.MSG_LOAD_STATE) { + putBoolean(NappletBrowserContract.KEY_IS_LOADING, false) + putBoolean(NappletBrowserContract.KEY_LOAD_FAILED, true) + putString(NappletBrowserContract.KEY_URL, tab.url) + } + } + return true + } } /** Tells the client whether a main-frame load is in flight and whether it failed, so it can overlay a spinner/retry. */ @@ -548,6 +896,7 @@ class NappletBrowserService : Service() { Bundle().apply { putString(NappletBrowserContract.KEY_URL, url) putBoolean(NappletBrowserContract.KEY_CAN_GO_BACK, view.canGoBack()) + putBoolean(NappletBrowserContract.KEY_CAN_GO_FORWARD, view.canGoForward()) title?.let { putString(NappletBrowserContract.KEY_TITLE, it) } } } @@ -568,21 +917,13 @@ class NappletBrowserService : Service() { onApplied() return } - val executor = java.util.concurrent.Executor { it.run() } + val executor = Executor { it.run() } runCatching { if (port > 0) { - val config = - androidx.webkit.ProxyConfig - .Builder() - .addProxyRule("socks5://127.0.0.1:$port") - .build() - androidx.webkit.ProxyController - .getInstance() - .setProxyOverride(config, executor) { onApplied() } + val config = ProxyConfig.Builder().addProxyRule("socks5://127.0.0.1:$port").build() + ProxyController.getInstance().setProxyOverride(config, executor) { onApplied() } } else { - androidx.webkit.ProxyController - .getInstance() - .clearProxyOverride(executor) { onApplied() } + ProxyController.getInstance().clearProxyOverride(executor) { onApplied() } } }.onFailure { Log.w(TAG, "Failed to apply WebView proxy override", it) @@ -607,6 +948,23 @@ class NappletBrowserService : Service() { val raw = message.data ?: return val envelope = parseJsonObjectOrNull(raw) ?: return + // Browser conveniences (share, blob downloads) are handled here, never brokered. The theme colour + // only matters to a window with system bars, which an embedded tab doesn't own. + when (envelope.stringOrNull("type")) { + "browser.share" -> { + BrowserWebTools.share(this, envelope.stringOrNull("title"), envelope.stringOrNull("text"), envelope.stringOrNull("url")) + return + } + "browser.download" -> { + val data = envelope.stringOrNull("data") ?: return + if (data.startsWith("data:") && data.length <= BrowserDownloads.MAX_INLINE_BYTES / 3 * 4 + 256) { + BrowserDownloads.saveDataUrl(this, data, envelope.stringOrNull("name")) + } + return + } + "browser.themeColor" -> return + } + // IME events aren't brokered — the main app hosts the keyboard. Relay the envelope to the client. if (envelope.stringOrNull("type").orEmpty().startsWith("ime.")) { val reply = diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserUiAdapter.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserUiAdapter.kt index 2ffb2b097e..82e0315008 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserUiAdapter.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserUiAdapter.kt @@ -29,7 +29,6 @@ import android.os.Bundle import android.os.Handler import android.os.Looper import android.view.View -import android.webkit.WebView import android.widget.FrameLayout import androidx.annotation.RequiresApi import androidx.privacysandbox.ui.core.SandboxedUiAdapter @@ -61,25 +60,29 @@ class NappletBrowserUiAdapter( // WebView creation must run on the main thread; openSession is called on a binder thread. mainHandler.post { runCatching { - val webView = service.createBrowserWebView(context, sessionId) + // The session's view is a container around the WebView, so HTML fullscreen can lay the + // page's custom view over it and a crashed renderer's WebView can be swapped for a new one. + val container = FrameLayout(context) + val webView = service.createBrowserWebView(context, sessionId, container) + container.addView(webView, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)) // FrameLayout.LayoutParams (a MarginLayoutParams) — the SurfaceControlViewHost container // measures children with measureChildWithMargins, which casts to MarginLayoutParams. - webView.layoutParams = FrameLayout.LayoutParams(initialWidth, initialHeight) - BrowserSession(sessionId, webView, service) + container.layoutParams = FrameLayout.LayoutParams(initialWidth, initialHeight) + BrowserSession(sessionId, container, service) }.onSuccess { session -> clientExecutor.execute { client.onSessionOpened(session) } } .onFailure { t -> clientExecutor.execute { client.onSessionError(t) } } } } } -/** A single embedded browser session: the WebView is the rendered view; close tears it down. */ +/** A single embedded browser session: the WebView's container is the rendered view; close tears it down. */ @RequiresApi(Build.VERSION_CODES.R) private class BrowserSession( private val sessionId: String, - private val webView: WebView, + private val container: FrameLayout, private val service: NappletBrowserService, ) : SandboxedUiAdapter.Session { - override val view: View get() = webView + override val view: View get() = container override val signalOptions: Set<String> = emptySet() @@ -91,8 +94,8 @@ private class BrowserSession( width: Int, height: Int, ) { - webView.layoutParams = FrameLayout.LayoutParams(width, height) - webView.requestLayout() + container.layoutParams = FrameLayout.LayoutParams(width, height) + container.requestLayout() } override fun notifyZOrderChanged(isZOrderOnTop: Boolean) { diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletControlSheet.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletControlSheet.kt index cdc8215eb2..d1546410bf 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletControlSheet.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletControlSheet.kt @@ -32,236 +32,275 @@ import android.view.Gravity import android.view.MotionEvent import android.view.View import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager import android.widget.EditText import android.widget.ImageView import android.widget.LinearLayout +import android.widget.ScrollView import android.widget.Switch import android.widget.TextView import androidx.core.content.ContextCompat -import androidx.core.net.toUri +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.Action +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.SectionKind +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.R as CommonsR /** - * The full-screen sandbox surfaces' **top pull-down sheet** — the native-View twin of the embedded - * tabs' Compose `TopControlSheet`. Collapsed it's just a small grabber centered at the very top edge, - * out of the corner where a site puts its own avatar/menu. Pull it down (or tap) to reveal the page's - * controls: route over Tor, reload, and "what it can access" (sandboxed apps). + * The full-screen surfaces' **top pull-down pill** — the plain-View twin of the embedded tabs' Compose + * `TopControlSheet`. Collapsed it's a small grabber centered at the top edge, out of the corner where a site + * puts its own avatar/menu. Pulled down (or tapped) it shows, like a Chrome PWA's app menu: * - * Built in code (no XML) because `:nappletHost` hosts plain Android `View`s, not Compose, and must stay - * dependency-light. Add it to a `FrameLayout` parent at `Gravity.TOP` filling the width; it manages its - * own expand/collapse. + * - a header with the page title, its origin + connection badge (tap: page info; long-press: copy link), + * and a close button; + * - the icon row (back · forward · reload/stop · star · share); + * - the menu rows, then Privacy and Developer groups. + * + * *Which* actions appear, and in what order, comes from [BrowserChrome] — the same source the Compose sheet + * uses — and each action's icon/label from [BrowserChromeLabels]. This class only draws them. The page + * can't draw over it. Built in code (no XML, no Compose/Material) because `:nappletHost` stays light. */ -@SuppressLint("UseSwitchCompatOrMaterialCode") // plain framework Switch: :nappletHost is Compose/Material-free +@SuppressLint("UseSwitchCompatOrMaterialCode", "ViewConstructor") class NappletControlSheet( context: Context, + initialState: BrowserChrome.State, title: String, - private val isSandbox: Boolean, - private val onReload: () -> Unit, - torInitiallyOn: Boolean?, - private val onToggleTor: (Boolean) -> Unit = {}, - // When non-null, the Tor row taps through to this (e.g. a confirm dialog that relaunches) instead of - // toggling inline — used by the nSite host, where switching routing rebuilds the whole session. - private val onNetworkTap: (() -> Unit)? = null, - private val onInfo: (() -> Unit)? = null, - // When non-null, a "Manage permissions" row is added that taps through to this — used to open the - // main process's editable Connected Apps detail screen for this surface. - private val onPermissions: (() -> Unit)? = null, - // The live URL of a plain-website browser. Non-null only for the direct-WebView browser (never an - // nsite/napplet), where it renders an editable address row; [onNavigate] loads what the user types. - liveUrl: String? = null, - private val onNavigate: ((String) -> Unit)? = null, - // When non-null, a "Console" toggle row is added to the pull-down sheet. The callback is invoked with - // the new visibility each time the user flips it; the count label is updated via [updateConsoleCount]. - private val onConsole: ((Boolean) -> Unit)? = null, - // When non-null, a favorite toggle row is shown; called with the current URL and new isFavorite state. + private val listener: Listener, isFavoriteInitially: Boolean = false, - private val onFavoriteToggle: ((url: String, isFavorite: Boolean) -> Unit)? = null, + /** Shows the header's close button (finishing the window). */ + private val showClose: Boolean = true, ) : LinearLayout(context) { + /** What the sheet asks its host to do. Every row and icon ends up in [onAction]. */ + interface Listener { + fun onAction(action: Action) + + /** The user typed an address into "Edit address" and pressed Go. */ + fun onNavigate(text: String) {} + + /** The text-size stepper moved to [percent]. */ + fun onTextZoom(percent: Int) {} + + /** The origin chip was tapped: show page info (or the access summary for sandboxed apps). */ + fun onOriginTap() {} + + fun onClose() {} + } + private val onSurface = resolveThemeColor(android.R.attr.textColorPrimary) private val dimmed = resolveThemeColor(android.R.attr.textColorSecondary) private val surface = resolveThemeColor(android.R.attr.colorBackground) + private val accent = resolveThemeColor(android.R.attr.colorPrimary) + private val glyphs: Typeface = BrowserGlyphs.typeface(context) + + var state: BrowserChrome.State = initialState + private set + private var title: String = title + private var isFavorite = isFavoriteInitially + private var desktopSite = false + private var textZoom = BrowserChrome.DEFAULT_TEXT_ZOOM + private var consoleShowing = false + private var consoleCount = 0 + private var editingAddress = false private var expanded = false - private var title = title - private var torOn = torInitiallyOn - private var currentUrl = liveUrl - private var isFavorite = isFavoriteInitially - private var consoleShowing = false - private val panel: LinearLayout - private var titleView: TextView? = null - private var grabber: View? = null - private var torLabel: TextView? = null - private var torSwitch: Switch? = null - private var addressField: EditText? = null - private var securityGlyph: TextView? = null - private var consoleLabel: TextView? = null - private var consoleSwitch: Switch? = null - private var favoriteLabel: TextView? = null + private val grabber: View init { orientation = VERTICAL gravity = Gravity.CENTER_HORIZONTAL - - panel = buildPanel().also { addView(it) } - addView(buildGrabber().also { grabber = it }) + panel = + LinearLayout(context).apply { + orientation = VERTICAL + visibility = View.GONE + elevation = dp(6).toFloat() + background = + GradientDrawable().apply { + cornerRadii = floatArrayOf(0f, 0f, 0f, 0f, dp(16).toFloat(), dp(16).toFloat(), dp(16).toFloat(), dp(16).toFloat()) + setColor(surface) + } + setPadding(dp(8), dp(6), dp(8), dp(10)) + } + addView(panel) + grabber = buildGrabber() + addView(grabber) } - private fun buildPanel(): LinearLayout = - LinearLayout(context).apply { - orientation = VERTICAL - visibility = View.GONE - elevation = dp(6).toFloat() - background = - GradientDrawable().apply { - cornerRadii = floatArrayOf(0f, 0f, 0f, 0f, dp(16).toFloat(), dp(16).toFloat(), dp(16).toFloat(), dp(16).toFloat()) - setColor(surface) - } - setPadding(dp(8), dp(6), dp(8), dp(10)) + // ---- state updates from the host ---- - addView(titleRow()) - // Browser only: an editable address bar showing the live URL + a security glyph. nsite/napplet - // hosts pass no navigate callback, so they never get one. - onNavigate?.let { addView(addressRow(currentUrl.orEmpty(), it)) } - addView(divider()) - if (torOn != null) addView(torRow()) - addView( - actionRow("↻", context.getString(R.string.napplet_chrome_reload)) { - collapse() - onReload() - }, - ) - onInfo?.let { info -> - addView( - actionRow("ⓘ", context.getString(R.string.napplet_chrome_permissions_desc)) { - collapse() - info() - }, - ) - } - onPermissions?.let { manage -> - addView( - actionRow("⚙", context.getString(R.string.napplet_chrome_manage_permissions)) { - collapse() - manage() - }, - ) - } - onConsole?.let { - val label = - TextView(context).apply { - text = context.getString(CommonsR.string.browser_console_title_short) - setTextColor(onSurface) - textSize = 15f - setPadding(dp(8), 0, 0, 0) - // Weight 1 so the label fills and shoves the Switch to the end, like the Tor row. - layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f) - } - consoleLabel = label - // Display-only switch (the whole row is the touch target), matching the Tor row + Compose twin. - val toggle = - Switch(context).apply { - isChecked = consoleShowing - isClickable = false - isFocusable = false - } - consoleSwitch = toggle - addView( - LinearLayout(context).apply { - orientation = HORIZONTAL - gravity = Gravity.CENTER_VERTICAL - setPadding(dp(8), dp(10), dp(8), dp(10)) - isClickable = true - setOnClickListener { toggleConsole() } - addView( - TextView(context).apply { - text = ">" - setTextColor(dimmed) - textSize = 18f - width = dp(28) - gravity = Gravity.CENTER - typeface = Typeface.MONOSPACE - }, - ) - addView(label) - addView(toggle) - }, - ) - } - onFavoriteToggle?.let { - val label = - TextView(context).apply { - text = context.getString(if (isFavorite) R.string.browser_favorite_remove else R.string.browser_favorite_add) - setTextColor(onSurface) - textSize = 15f - setPadding(dp(8), 0, 0, 0) - } - favoriteLabel = label - addView( - LinearLayout(context).apply { - orientation = HORIZONTAL - gravity = Gravity.CENTER_VERTICAL - setPadding(dp(8), dp(10), dp(8), dp(10)) - isClickable = true - setOnClickListener { toggleFavorite() } - addView( - TextView(context).apply { - text = "★" - setTextColor(dimmed) - textSize = 18f - width = dp(28) - gravity = Gravity.CENTER - }, - ) - addView(label) - }, - ) + /** + * The page navigated. Moving to another site swaps the title for that site's host until its page title + * arrives ([updateTitle]), and marks the pin state unknown until the host answers ([setFavorite]). + */ + fun updateUrl(url: String) { + val previous = state.url + if (url == previous) return + state = state.copy(url = url) + if (BrowserChrome.displayHost(url) != BrowserChrome.displayHost(previous)) title = BrowserChrome.displayHost(url) + // Unknown until the host answers; "Add" meanwhile. The toggle sends an explicit target state, so a + // tap in that window can only add (idempotent), never silently remove an existing pin. + isFavorite = false + refresh() + } + + /** Shows the page's `<title>`, falling back to the host (WebView reports the URL for untitled pages). */ + fun updateTitle(pageTitle: String?) { + val real = pageTitle?.trim()?.takeIf { it.isNotEmpty() && it != state.url } + title = real ?: BrowserChrome.displayHost(state.url) + refresh() + } + + /** Applies the registry's answer for [url]; ignored once the user has moved on to another page. */ + fun setFavorite( + url: String, + favorite: Boolean, + ) { + if (url != state.url) return + isFavorite = favorite + refresh() + } + + fun setNavigation( + canGoBack: Boolean, + canGoForward: Boolean, + ) = update(state.copy(canGoBack = canGoBack, canGoForward = canGoForward)) + + fun setLoading(loading: Boolean) = update(state.copy(isLoading = loading)) + + fun setTor(on: Boolean) = update(state.copy(torOn = on)) + + fun setDesktopSite(on: Boolean) { + desktopSite = on + refresh() + } + + fun setTextZoom(percent: Int) { + textZoom = percent + refresh() + } + + fun setConsoleShowing(showing: Boolean) { + consoleShowing = showing + refresh() + } + + fun updateConsoleCount(count: Int) { + consoleCount = count + refresh() + } + + private fun update(next: BrowserChrome.State) { + if (next == state) return + state = next + refresh() + } + + /** Rebuilds the open panel. Collapsed, nothing is drawn, so it waits for the next [expand]. */ + private fun refresh() { + grabber.contentDescription = title + if (expanded) render() + } + + // ---- drawing ---- + + private fun render() { + panel.removeAllViews() + panel.addView(header()) + panel.addView(iconRow()) + panel.addView(divider()) + val sections = + LinearLayout(context).apply { + orientation = VERTICAL + BrowserChrome.sections(state).forEachIndexed { index, section -> + if (index > 0) addView(divider()) + sectionTitle(section.kind)?.let { addView(sectionLabel(it)) } + section.actions.forEach { addView(row(it)) } + } } + panel.addView( + MaxHeightScrollView(context, (resources.displayMetrics.heightPixels * 0.6f).toInt()).apply { + isVerticalScrollBarEnabled = false + addView(sections) + }, + ) + } + + private fun sectionTitle(kind: SectionKind): String? = + when (kind) { + SectionKind.PAGE -> null + SectionKind.PRIVACY -> context.getString(CommonsR.string.browser_section_privacy) + SectionKind.DEVELOPER -> context.getString(CommonsR.string.browser_section_developer) } - private fun titleRow(): View = + private fun header(): View = LinearLayout(context).apply { orientation = HORIZONTAL gravity = Gravity.CENTER_VERTICAL - setPadding(dp(8), dp(8), dp(8), dp(8)) - addView( - TextView(context).apply { - text = if (isSandbox) "🛡" else "🌐" - textSize = 16f - }, - ) - addView( - TextView(context) - .apply { - text = title - setTextColor(onSurface) - textSize = 16f - maxLines = 1 - ellipsize = TextUtils.TruncateAt.END - setPadding(dp(10), 0, 0, 0) - }.also { titleView = it }, - ) + setPadding(dp(8), dp(6), dp(4), dp(6)) + val security = BrowserChrome.security(state) + addView(iconView(BrowserChromeLabels.securitySymbol(security), BrowserChromeLabels.securityDrawable(security), dimmed, 20)) + if (editingAddress) { + addView(addressField()) + } else { + addView( + LinearLayout(context).apply { + orientation = VERTICAL + setPadding(dp(12), 0, dp(8), 0) + layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f) + isClickable = true + setOnClickListener { + collapse() + listener.onOriginTap() + } + if (!state.isSandbox) { + setOnLongClickListener { + listener.onAction(Action.COPY_LINK) + true + } + } + addView( + TextView(context).apply { + text = title + setTextColor(onSurface) + textSize = 16f + typeface = Typeface.DEFAULT_BOLD + maxLines = 1 + ellipsize = TextUtils.TruncateAt.END + }, + ) + addView( + TextView(context).apply { + text = + if (state.isSandbox) { + context.getString(BrowserChromeLabels.securityLabel(security)) + } else { + BrowserChrome.displayHost(state.url) + " · " + context.getString(BrowserChromeLabels.securityLabel(security)) + } + setTextColor(dimmed) + textSize = 13f + maxLines = 1 + ellipsize = TextUtils.TruncateAt.MIDDLE + }, + ) + }, + ) + } + if (showClose) { + addView( + glyphButton(BrowserChromeLabels.symbolFor(Action.STOP)!!, context.getString(CommonsR.string.browser_action_close), onSurface) { + collapse() + listener.onClose() + }, + ) + } } - /** - * The browser address bar: a security glyph (🧅 Tor / 🔒 https / 🌐 plain) + an editable URL field. - * Pressing Go hands the trimmed text to [onNavigate] (normalized by the caller) and collapses the sheet. - */ - private fun addressRow( - initial: String, - onNavigate: (String) -> Unit, - ): View { - val glyph = - TextView(context).apply { - text = securityGlyphFor(initial) - textSize = 15f - width = dp(28) - gravity = Gravity.CENTER - } - securityGlyph = glyph + /** The rarely used editable address, swapped in for the origin chip by the "Edit address" row. */ + private fun addressField(): View { val field = EditText(context).apply { - setText(initial) + setText(state.url) setTextColor(onSurface) setHintTextColor(dimmed) hint = context.getString(CommonsR.string.browser_address_hint) @@ -272,208 +311,232 @@ class NappletControlSheet( background = null inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI imeOptions = EditorInfo.IME_ACTION_GO - layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f) + layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f).apply { marginStart = dp(8) } setOnEditorActionListener { v, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_GO) { - val text = - v.text - ?.toString() - ?.trim() - .orEmpty() - if (text.isNotEmpty()) { - clearFocus() - collapse() - onNavigate(text) - } - true - } else { - false + if (actionId != EditorInfo.IME_ACTION_GO) return@setOnEditorActionListener false + val text = + v.text + ?.toString() + ?.trim() + .orEmpty() + if (text.isNotEmpty()) { + hideKeyboard(v) + editingAddress = false + collapse() + listener.onNavigate(text) } + true } } - addressField = field - return LinearLayout(context).apply { - orientation = HORIZONTAL - gravity = Gravity.CENTER_VERTICAL - setPadding(dp(8), dp(8), dp(8), dp(8)) - addView(glyph) - addView(field) + field.post { + field.requestFocus() + context.getSystemService(InputMethodManager::class.java)?.showSoftInput(field, 0) } + return field } - /** Updates the count shown in the Console row label so the user sees how many messages are waiting. */ - fun updateConsoleCount(count: Int) { - consoleLabel?.text = - if (count > 0) { - context.getString(CommonsR.string.browser_console_title, count) - } else { - context.getString(CommonsR.string.browser_console_title_short) - } - } - - /** - * Refreshes the address bar + security glyph as the page navigates. No-op without an address row. - * Moving to another site also swaps the title for that site's host until its page title arrives - * ([updateTitle]), so the sheet never names the site the user already left. - */ - fun updateUrl(url: String) { - val previous = currentUrl - currentUrl = url - // Don't fight the user while they're editing the field. - addressField?.takeIf { !it.hasFocus() }?.setText(url) - securityGlyph?.text = securityGlyphFor(url) - if (hostOf(url) != previous?.let(::hostOf)) setTitleText(hostOf(url) ?: url) - // A different page: its pin state is unknown until the host answers through [setFavorite]. Show - // "Add" meanwhile — the toggle sends the explicit target state, so even a tap in that window can - // only ever add (idempotent), never silently remove an existing pin. - if (url != previous && onFavoriteToggle != null) applyFavorite(false) - } - - /** - * Shows the page's own `<title>`, falling back to the current host when the page has none (WebView - * reports the URL itself as the title of an untitled document). - */ - fun updateTitle(pageTitle: String?) { - val real = pageTitle?.trim()?.takeIf { it.isNotEmpty() && it != currentUrl } - setTitleText(real ?: currentUrl?.let(::hostOf) ?: return) - } - - /** Applies the registry's answer for [url]; ignored when the user has already moved to another page. */ - fun setFavorite( - url: String, - favorite: Boolean, - ) { - if (url == currentUrl) applyFavorite(favorite) - } - - private fun applyFavorite(favorite: Boolean) { - isFavorite = favorite - favoriteLabel?.text = context.getString(if (favorite) R.string.browser_favorite_remove else R.string.browser_favorite_add) - } - - private fun setTitleText(text: String) { - title = text - titleView?.text = text - grabber?.contentDescription = text - } - - private fun hostOf(url: String): String? = runCatching { url.toUri().host }.getOrNull()?.takeIf { it.isNotBlank() } - - private fun toggleFavorite() { - val url = currentUrl?.takeIf { it.isNotBlank() } ?: return - applyFavorite(!isFavorite) - collapse() - onFavoriteToggle?.invoke(url, isFavorite) - } - - private fun securityGlyphFor(url: String): String = - when { - torOn == true -> "🧅" // 🧅 routed over Tor - url.startsWith("https://", ignoreCase = true) -> "🔒" // 🔒 secure - else -> "🌐" // 🌐 plain http - } - - private fun torRow(): View { - // Steady, muted icon (the Switch carries the on/off state) — matches the Compose twin, where the - // lock icon is a constant onSurfaceVariant tint and the Switch is the state indicator. - val icon = - ImageView(context).apply { - setImageResource(R.drawable.ic_tor) - setColorFilter(dimmed) - layoutParams = LayoutParams(dp(22), dp(22)) - } - val label = - TextView(context).apply { - text = context.getString(if (torOn == true) R.string.napplet_net_tor_label else R.string.napplet_net_open_label) - setTextColor(onSurface) - textSize = 15f - setPadding(dp(14), 0, 0, 0) - // Weight 1 so the label fills and shoves the Switch to the end, like the Compose row. - layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f) - } - // Display-only: the whole row is the touch target (parity with the Compose row, whose Switch and - // row both route to the same onToggle), so the Switch itself doesn't take clicks. - val toggle = - Switch(context).apply { - isChecked = torOn == true - isClickable = false - isFocusable = false - } - torLabel = label - torSwitch = toggle - return LinearLayout(context).apply { - orientation = HORIZONTAL - gravity = Gravity.CENTER_VERTICAL - setPadding(dp(8), dp(10), dp(8), dp(10)) - isClickable = true - setOnClickListener { - if (onNetworkTap != null) { - collapse() - onNetworkTap.invoke() - } else { - toggleTor() - } - } - addView(icon) - addView(label) - addView(toggle) - } - } - - private fun toggleTor() { - val next = !(torOn ?: return) - torOn = next - torSwitch?.isChecked = next - torLabel?.text = context.getString(if (next) R.string.napplet_net_tor_label else R.string.napplet_net_open_label) - securityGlyph?.text = securityGlyphFor(currentUrl.orEmpty()) - onToggleTor(next) - } - - private fun toggleConsole() { - consoleShowing = !consoleShowing - consoleSwitch?.isChecked = consoleShowing - // Collapse the top sheet on toggle, like the Compose twin, so the bottom console isn't hidden behind it. - collapse() - onConsole?.invoke(consoleShowing) - } - - private fun actionRow( - glyph: String, - label: String, - onClick: () -> Unit, - ): View = + private fun iconRow(): View = LinearLayout(context).apply { orientation = HORIZONTAL gravity = Gravity.CENTER_VERTICAL - // Same vertical rhythm as the Tor row and the Compose twin's rows. + setPadding(0, dp(2), 0, dp(4)) + BrowserChrome.iconRow(state).forEach { action -> + val enabled = BrowserChrome.isEnabled(state, action) + val tint = if (action == Action.FAVORITE && isFavorite) accent else onSurface + val button = + glyphButton( + BrowserChromeLabels.symbolFor(action)!!, + context.getString(BrowserChromeLabels.labelFor(action, isFavorite = isFavorite)), + tint, + ) { + collapse() + onRowAction(action) + } + if (action == Action.FAVORITE && isFavorite) button.typeface = BrowserGlyphs.filledTypeface(context) + button.isEnabled = enabled + button.alpha = if (enabled) 1f else 0.35f + button.layoutParams = LayoutParams(0, dp(48), 1f) + addView(button) + } + } + + private fun row(action: Action): View { + val label = + when (action) { + Action.BACK_TO_APP -> context.getString(CommonsR.string.browser_action_back_to_app, BrowserChrome.displayHost(state.startUrl)) + else -> context.getString(BrowserChromeLabels.labelFor(action, isFavorite = isFavorite, torOn = state.torOn == true)) + } + val consoleLabel = if (action == Action.CONSOLE && consoleCount > 0) context.getString(CommonsR.string.browser_console_title, consoleCount) else label + return LinearLayout(context).apply { + orientation = HORIZONTAL + gravity = Gravity.CENTER_VERTICAL setPadding(dp(8), dp(10), dp(8), dp(10)) - isClickable = true - setOnClickListener { onClick() } + addView(iconView(BrowserChromeLabels.symbolFor(action), BrowserChromeLabels.drawableFor(action), dimmed, 22)) addView( TextView(context).apply { - text = glyph - setTextColor(dimmed) - textSize = 18f - width = dp(28) - gravity = Gravity.CENTER - }, - ) - addView( - TextView(context).apply { - text = label + text = consoleLabel setTextColor(onSurface) textSize = 15f - setPadding(dp(8), 0, 0, 0) + setPadding(dp(14), 0, 0, 0) + layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f) }, ) + if (action == Action.TEXT_SIZE) { + addView(textSizeStepper()) + } else { + isClickable = true + setOnClickListener { + if (!BrowserChromeLabels.keepsSheetOpen(action)) collapse() + onRowAction(action) + } + if (BrowserChromeLabels.isToggle(action)) { + // Display-only: the whole row is the touch target, as in the Compose twin. + addView( + Switch(context).apply { + isChecked = + when (action) { + Action.TOR -> state.torOn == true + Action.DESKTOP_SITE -> desktopSite + else -> consoleShowing + } + isClickable = false + isFocusable = false + }, + ) + } + } + } + } + + private fun textSizeStepper(): View = + LinearLayout(context).apply { + orientation = HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + addView( + glyphButton(MaterialSymbols.Remove, context.getString(CommonsR.string.browser_action_text_smaller), onSurface) { + textZoom = BrowserChrome.stepTextZoom(textZoom, larger = false) + listener.onTextZoom(textZoom) + refresh() + }, + ) + addView( + TextView(context).apply { + text = context.getString(CommonsR.string.browser_action_text_size_value, textZoom) + setTextColor(onSurface) + textSize = 14f + gravity = Gravity.CENTER + minWidth = dp(48) + }, + ) + addView( + glyphButton(MaterialSymbols.Add, context.getString(CommonsR.string.browser_action_text_larger), onSurface) { + textZoom = BrowserChrome.stepTextZoom(textZoom, larger = true) + listener.onTextZoom(textZoom) + refresh() + }, + ) + } + + private fun onRowAction(action: Action) { + when (action) { + Action.EDIT_ADDRESS -> { + editingAddress = true + refresh() + } + Action.FAVORITE -> { + isFavorite = !isFavorite + listener.onAction(action) + } + Action.DESKTOP_SITE -> { + desktopSite = !desktopSite + listener.onAction(action) + } + Action.CONSOLE -> { + consoleShowing = !consoleShowing + listener.onAction(action) + } + else -> listener.onAction(action) + } + } + + /** The favorite state the user is asking for (read by the host right after a FAVORITE action). */ + fun wantsFavorite(): Boolean = isFavorite + + private fun sectionLabel(text: String): View = + TextView(context).apply { + this.text = text + setTextColor(dimmed) + textSize = 12f + isAllCaps = true + letterSpacing = 0.06f + setPadding(dp(8), dp(10), dp(8), dp(2)) + } + + private fun iconView( + symbol: MaterialSymbol?, + drawable: Int?, + tint: Int, + sizeDp: Int, + ): View = + if (drawable != null) { + ImageView(context).apply { + setImageResource(drawable) + setColorFilter(tint) + layoutParams = LayoutParams(dp(sizeDp), dp(sizeDp)) + } + } else { + glyphText(symbol?.glyph.orEmpty(), tint, sizeDp).apply { layoutParams = LayoutParams(dp(sizeDp + 4), LayoutParams.WRAP_CONTENT) } + } + + private fun glyphText( + glyph: String, + tint: Int, + sizeDp: Int, + ): TextView = + TextView(context).apply { + text = glyph + typeface = glyphs + setTextColor(tint) + setTextSize(TypedValue.COMPLEX_UNIT_DIP, sizeDp.toFloat()) + gravity = Gravity.CENTER + includeFontPadding = false + } + + private fun glyphButton( + symbol: MaterialSymbol, + description: String, + tint: Int, + onClick: () -> Unit, + ): TextView = + glyphText(symbol.glyph, tint, 22).apply { + contentDescription = description + tooltipText = description + minWidth = dp(44) + minHeight = dp(44) + isClickable = true + isFocusable = true + background = selectableBackground() + // Mirrored glyphs (back/forward) flip for right-to-left layouts, as Compose's autoMirror does. + if (symbol.autoMirror && resources.configuration.layoutDirection == LAYOUT_DIRECTION_RTL) scaleX = -1f + setOnClickListener { onClick() } + } + + private fun selectableBackground() = + TypedValue().let { tv -> + context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, tv, true) + ContextCompat.getDrawable(context, tv.resourceId) } private fun divider(): View = View(context).apply { - setBackgroundColor(dimmed and 0x33FFFFFF.toInt()) - layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dp(1)) + setBackgroundColor(dimmed and 0x33FFFFFF) + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dp(1)).apply { setMargins(0, dp(2), 0, dp(2)) } } + private fun hideKeyboard(view: View) { + context.getSystemService(InputMethodManager::class.java)?.hideSoftInputFromWindow(view.windowToken, 0) + } + /** The grabber: a small rounded bar centered at the top edge; tap toggles, vertical drag opens/closes. */ @SuppressLint("ClickableViewAccessibility") private fun buildGrabber(): View { @@ -489,12 +552,7 @@ class NappletControlSheet( return LinearLayout(context).apply { orientation = VERTICAL gravity = Gravity.CENTER_HORIZONTAL - // Wrap the grabber (a vertical LinearLayout defaults its children to MATCH_PARENT width, which - // would stretch this chip's background across the whole screen) and center it under the parent. - layoutParams = - LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply { - gravity = Gravity.CENTER_HORIZONTAL - } + layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply { gravity = Gravity.CENTER_HORIZONTAL } setPadding(dp(16), dp(7), dp(16), dp(7)) background = GradientDrawable().apply { @@ -541,16 +599,24 @@ class NappletControlSheet( } } - private fun expand() { + val isExpanded: Boolean get() = expanded + + fun expand() { if (expanded) return expanded = true + render() panel.visibility = View.VISIBLE } - private fun collapse() { + fun collapse() { if (!expanded) return expanded = false + if (editingAddress) { + editingAddress = false + hideKeyboard(panel) + } panel.visibility = View.GONE + panel.removeAllViews() } private fun withAlpha( @@ -565,4 +631,17 @@ class NappletControlSheet( } private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt() + + /** A [ScrollView] that stops growing at [maxHeightPx], so a long menu scrolls instead of covering the page. */ + private class MaxHeightScrollView( + context: Context, + private val maxHeightPx: Int, + ) : ScrollView(context) { + override fun onMeasure( + widthMeasureSpec: Int, + heightMeasureSpec: Int, + ) { + super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(maxHeightPx, MeasureSpec.AT_MOST)) + } + } } diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt index fd5646c0a8..ca4d8ddc56 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt @@ -39,6 +39,7 @@ import android.view.KeyEvent import android.view.View import android.view.ViewGroup import android.webkit.ConsoleMessage +import android.webkit.RenderProcessGoneDetail import android.webkit.ValueCallback import android.webkit.WebChromeClient import android.webkit.WebResourceError @@ -62,6 +63,7 @@ import androidx.webkit.ProxyController import androidx.webkit.WebMessageCompat import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome import com.vitorpamplona.amethyst.commons.napplet.NappletWebContract import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletProtocolJson import com.vitorpamplona.amethyst.commons.util.booleanOrNull @@ -195,6 +197,11 @@ class NappletHostActivity : ComponentActivity() { // Bottom pull-up developer console: the page's console.log/warn/error plus any resource load errors. private var consolePanel: NappletConsolePanel? = null private var controlSheet: NappletControlSheet? = null + private var findBar: BrowserFindBar? = null + private var consoleShowing = false + + // Set when the renderer died and the WebView was destroyed, so teardown doesn't touch it again. + private var webViewGone = false // Set once the WebView has begun loading the shell, so a retry doesn't reload it. private var started = false @@ -204,7 +211,9 @@ class NappletHostActivity : ComponentActivity() { private val backCallback = object : OnBackPressedCallback(false) { override fun handleOnBackPressed() { - if (this@NappletHostActivity::webView.isInitialized && webView.canGoBack()) { + if (findBar?.isShowing == true) { + findBar?.hide() + } else if (this@NappletHostActivity::webView.isInitialized && !webViewGone && webView.canGoBack()) { webView.goBack() } else { isEnabled = false @@ -215,7 +224,8 @@ class NappletHostActivity : ComponentActivity() { /** Keep the in-WebView back gesture enabled exactly while the applet has history to pop. */ private fun syncBackState() { - if (this::webView.isInitialized) backCallback.isEnabled = webView.canGoBack() + val canGoBack = this::webView.isInitialized && !webViewGone && webView.canGoBack() + backCallback.isEnabled = canGoBack || findBar?.isShowing == true } // True between onResume and onPause. Sent to the broker (foreground hold) on connect too, in case @@ -329,6 +339,11 @@ class NappletHostActivity : ComponentActivity() { Gravity.BOTTOM, ), ) + addView( + BrowserFindBar(this@NappletHostActivity, { if (this@NappletHostActivity::webView.isInitialized && !webViewGone) webView else null }) { syncBackState() } + .also { findBar = it }, + FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM), + ) // Added last so the thin loading bar paints above the content (and over the grabber's top // edge); it's GONE except while loading, so it never obscures the trusted chrome. addView(topProgressBar) @@ -378,7 +393,7 @@ class NappletHostActivity : ComponentActivity() { override fun onResume() { super.onResume() - if (this::webView.isInitialized) { + if (this::webView.isInitialized && !webViewGone) { webView.onResume() } // Launching this :napplet-process surface backgrounded the main process; tell the broker to @@ -392,7 +407,7 @@ class NappletHostActivity : ComponentActivity() { // Foreground-only: stop the applet's JS/timers in the background so it cannot fire a // sign/decrypt/pay request whose consent prompt would surface over (and be confused with) // Amethyst's own UI. Requests only happen while the user is looking at this napplet. - if (this::webView.isInitialized) { + if (this::webView.isInitialized && !webViewGone) { // webView.onPause() pauses THIS WebView's JS/DOM (the security goal — a backgrounded napplet can't // fire a sign/decrypt/pay request). Do NOT call pauseTimers(): it's process-global and freezes // EVERY WebView in `:napplet`, including the embedded browser/napplet surfaces, which never resume. @@ -465,7 +480,7 @@ class NappletHostActivity : ComponentActivity() { // A picker still up when the applet is torn down would otherwise leave its callback unanswered. pendingFileChooser.cancel() fileChooserLauncher.teardown() - if (this::webView.isInitialized) { + if (this::webView.isInitialized && !webViewGone) { // Detach before destroy(): destroying an attached WebView corrupts the shared multiprocess // renderer/network state and breaks the other (embedded) WebViews in this `:napplet` process // (dead DNS, empty DOM reads, dead selection paint, broken IME). See NappletBrowserActivity. @@ -644,6 +659,26 @@ class NappletHostActivity : ComponentActivity() { logConsoleError(request, getString(R.string.napplet_console_http_error, errorResponse.statusCode, errorResponse.reasonPhrase.orEmpty())) } + /** + * The renderer died. Every WebView in `:napplet` shares one renderer and an unhandled crash kills + * the whole process (every embedded tab too), so drop only this WebView and offer to start over. + */ + override fun onRenderProcessGone( + view: WebView, + detail: RenderProcessGoneDetail, + ): Boolean { + Log.w(TAG) { "Renderer gone (crashed=${detail.didCrash()}); offering a restart" } + webViewGone = true + findBar?.hide() + (view.parent as? ViewGroup)?.removeView(view) + view.destroy() + loadingView?.let { contentFrame.removeView(it) } + loadingView = null + contentFrame.addView(buildErrorView { recreate() }) + syncBackState() + return true + } + override fun shouldOverrideUrlLoading( view: WebView, request: WebResourceRequest, @@ -910,18 +945,56 @@ class NappletHostActivity : ComponentActivity() { private fun buildControlSheet(): View = NappletControlSheet( context = this, + initialState = + BrowserChrome.State( + surface = if (profile == HostProfile.WEBSITE) BrowserChrome.Surface.NSITE else BrowserChrome.Surface.NAPPLET, + presentation = BrowserChrome.Presentation.FULL_SCREEN, + url = "", + startUrl = "", + // Website-mode nSites can re-route over Tor; switching rebuilds the session, so the row + // taps through to a full relaunch rather than toggling inline. + torOn = if (profile.exposesNetwork && proxyPort > 0) useTor else null, + canFavorite = false, + hasAccessInfo = true, + ), title = barTitle(), - isSandbox = true, - onReload = { if (this::webView.isInitialized) webView.reload() }, - // Website-mode nSites can re-route over Tor; switching rebuilds the session, so the row taps - // through to a full relaunch rather than toggling inline. - torInitiallyOn = if (profile.exposesNetwork && proxyPort > 0) useTor else null, - onNetworkTap = if (profile.exposesNetwork && proxyPort > 0) ({ setNetworkMode(!useTor) }) else null, - onInfo = { showAccessDialog() }, - onPermissions = { openPermissions() }, - onConsole = { show -> consolePanel?.setShowing(show) }, + listener = + object : NappletControlSheet.Listener { + override fun onAction(action: BrowserChrome.Action) { + val wv = if (this@NappletHostActivity::webView.isInitialized && !webViewGone) webView else null + when (action) { + BrowserChrome.Action.RELOAD -> wv?.reload() + BrowserChrome.Action.STOP -> wv?.stopLoading() + BrowserChrome.Action.FIND_IN_PAGE -> { + setConsoleShowing(false) + findBar?.show() + syncBackState() + } + BrowserChrome.Action.TOR -> setNetworkMode(!useTor) + BrowserChrome.Action.ACCESS_INFO -> showAccessDialog() + BrowserChrome.Action.SITE_SETTINGS -> openPermissions() + BrowserChrome.Action.CONSOLE -> setConsoleShowing(!consoleShowing) + else -> Unit + } + } + + override fun onTextZoom(percent: Int) { + if (this@NappletHostActivity::webView.isInitialized && !webViewGone) BrowserWebTools.setTextZoom(webView, percent) + } + + override fun onOriginTap() = showAccessDialog() + + override fun onClose() = finish() + }, ).also { controlSheet = it } + private fun setConsoleShowing(showing: Boolean) { + consoleShowing = showing + if (showing) findBar?.hide() + consolePanel?.setShowing(showing) + controlSheet?.setConsoleShowing(showing) + } + /** * Ask the broker to open this napplet's editable permission screen. The sandbox can't state its own * coordinate, so we send only the launch token; the broker resolves it to the trusted coordinate and diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostService.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostService.kt index 5c73f25c81..b3a69b8d13 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostService.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostService.kt @@ -37,8 +37,10 @@ import android.os.Message import android.os.Messenger import android.os.SystemClock import android.view.View +import android.view.ViewGroup import android.webkit.JsPromptResult import android.webkit.JsResult +import android.webkit.RenderProcessGoneDetail import android.webkit.ValueCallback import android.webkit.WebChromeClient import android.webkit.WebResourceError @@ -47,6 +49,7 @@ import android.webkit.WebResourceResponse import android.webkit.WebSettings import android.webkit.WebView import android.webkit.WebViewClient +import android.widget.FrameLayout import androidx.annotation.RequiresApi import androidx.core.graphics.createBitmap import androidx.core.net.toUri @@ -113,6 +116,9 @@ class NappletHostService : Service() { // createHostWebView — @Volatile gives the happens-before so the worker never sees a stale null. @Volatile var contentServer: NappletContentServer? = null var webView: WebView? = null + + // The session's root view; a WebView lost to a renderer crash is rebuilt inside it on retry. + var container: FrameLayout? = null var bridgeReplyProxy: JavaScriptReplyProxy? = null var fireSeq = 0 @@ -185,7 +191,17 @@ class NappletHostService : Service() { replyWithAdapter(tab) } NappletEmbedContract.MSG_BACK -> tabFor(msg)?.webView?.let { if (it.canGoBack()) it.goBack() } - NappletEmbedContract.MSG_RELOAD -> tabFor(msg)?.webView?.reload() + NappletEmbedContract.MSG_RELOAD -> { + val tab = tabFor(msg) ?: return true + val container = tab.container + // After a renderer crash the tab has no WebView: the retry builds a fresh one. + if (tab.webView == null && container != null) { + val wv = createHostWebView(container.context, tab.sessionId, container) + container.addView(wv, 0, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)) + } else { + tab.webView?.reload() + } + } // onPause()/onResume() are per-WebView (pause/resume THIS surface's JS/DOM). Do NOT call // pauseTimers()/resumeTimers(): they are process-global and would freeze/thaw every WebView in // `:napplet` (the browser embed + other napplets), whose lifecycles are independent of this one. @@ -311,10 +327,14 @@ class NappletHostService : Service() { fun createHostWebView( context: Context, sessionId: String, + container: FrameLayout, ): WebView { // The session may have been closed between MSG_CREATE_SESSION and this posted call — fail rather // than build a WebView that no tab tracks (it would leak). val tab = tabs[sessionId] ?: error("No napplet tab for session $sessionId") + tab.container = container + // A rebuild after a renderer crash: release the previous content server first. + tab.contentServer?.close() val wv = WebView(nightThemedContext(context, tab.themeType)) // FIRST touch after construction: setProfile throws once the WebView has loaded content (or its // profile has otherwise been used), so the storage partition must be chosen before the @@ -357,6 +377,7 @@ class NappletHostService : Service() { tab.contentServer = null tab.webView?.destroy() tab.webView = null + tab.container = null } @Suppress("SetJavaScriptEnabled") @@ -533,6 +554,27 @@ class NappletHostService : Service() { pushLoadState(tab, isLoading = false) } + /** + * The renderer died. It is shared by every WebView in `:napplet`, and an unhandled crash kills the + * whole process — every other tab included. Drop just this tab's WebView and report the load as + * failed; the tab's retry (MSG_RELOAD) rebuilds it in the same surface. + */ + override fun onRenderProcessGone( + view: WebView, + detail: RenderProcessGoneDetail, + ): Boolean { + Log.w(TAG) { "Renderer gone (crashed=${detail.didCrash()}) for an embedded napplet/nsite" } + (view.parent as? ViewGroup)?.removeView(view) + view.destroy() + if (tab.webView === view) { + tab.webView = null + tab.bridgeReplyProxy = null + tab.loadFailed = true + pushLoadState(tab, isLoading = false) + } + return true + } + override fun shouldOverrideUrlLoading( view: WebView, request: WebResourceRequest, diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostUiAdapter.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostUiAdapter.kt index 356cb58027..5bdf866d02 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostUiAdapter.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostUiAdapter.kt @@ -29,7 +29,6 @@ import android.os.Bundle import android.os.Handler import android.os.Looper import android.view.View -import android.webkit.WebView import android.widget.FrameLayout import androidx.annotation.RequiresApi import androidx.privacysandbox.ui.core.SandboxedUiAdapter @@ -62,25 +61,29 @@ class NappletHostUiAdapter( // WebView creation must run on the main thread; openSession is called on a binder thread. mainHandler.post { runCatching { - val webView = service.createHostWebView(context, sessionId) + // The session's view is a container around the WebView, so a WebView lost to a renderer + // crash can be replaced by a fresh one in the same surface. + val container = FrameLayout(context) + val webView = service.createHostWebView(context, sessionId, container) + container.addView(webView, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)) // FrameLayout.LayoutParams (a MarginLayoutParams) — the SurfaceControlViewHost container // measures children with measureChildWithMargins, which casts to MarginLayoutParams. - webView.layoutParams = FrameLayout.LayoutParams(initialWidth, initialHeight) - HostSession(sessionId, webView, service) + container.layoutParams = FrameLayout.LayoutParams(initialWidth, initialHeight) + HostSession(sessionId, container, service) }.onSuccess { session -> clientExecutor.execute { client.onSessionOpened(session) } } .onFailure { t -> clientExecutor.execute { client.onSessionError(t) } } } } } -/** A single embedded napplet/nsite session: the WebView is the rendered view; close tears it down. */ +/** A single embedded napplet/nsite session: the WebView's container is the rendered view; close tears it down. */ @RequiresApi(Build.VERSION_CODES.R) private class HostSession( private val sessionId: String, - private val webView: WebView, + private val container: FrameLayout, private val service: NappletHostService, ) : SandboxedUiAdapter.Session { - override val view: View get() = webView + override val view: View get() = container override val signalOptions: Set<String> = emptySet() @@ -92,8 +95,8 @@ private class HostSession( width: Int, height: Int, ) { - webView.layoutParams = FrameLayout.LayoutParams(width, height) - webView.requestLayout() + container.layoutParams = FrameLayout.LayoutParams(width, height) + container.requestLayout() } override fun notifyZOrderChanged(isZOrderOnTop: Boolean) { diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletIpc.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletIpc.kt index 647c329cf8..3aeafdf70b 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletIpc.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletIpc.kt @@ -134,6 +134,29 @@ object NappletIpc { */ const val MSG_WEB_FAVORITE_STATE = 15 + /** + * Host → broker (browser mode): the remembered camera / microphone / location answers for + * [KEY_BROWSER_ORIGIN]. Carries [KEY_REQUEST_ID] (a long) for correlation; the broker answers `replyTo` + * with [MSG_SITE_PERMISSIONS], one `perm.<key>` string per permission holding its + * `BrowserSitePermission.Decision` name. + */ + const val MSG_QUERY_SITE_PERMISSIONS = 16 + + /** Broker → host: the reply to [MSG_QUERY_SITE_PERMISSIONS]. */ + const val MSG_SITE_PERMISSIONS = 17 + + /** + * Host → broker (browser mode): remember the user's answer ([KEY_SITE_DECISION]) for one permission + * ([KEY_SITE_PERMISSION], a `BrowserSitePermission.key`) on [KEY_BROWSER_ORIGIN]. Fire-and-forget. + */ + const val MSG_SET_SITE_PERMISSION = 18 + + /** + * Host → broker (browser mode): pin a launcher shortcut to [KEY_FAVORITE_URL] labelled + * [KEY_FAVORITE_LABEL]. The shortcut opens the page full screen in Amethyst's browser. Fire-and-forget. + */ + const val MSG_ADD_TO_HOME_SCREEN = 19 + const val KEY_REQUEST_ID = "requestId" const val KEY_PAYLOAD = "payload" @@ -161,6 +184,15 @@ object NappletIpc { /** Boolean: whether [KEY_FAVORITE_URL] is (or should become) a favorite. */ const val KEY_FAVORITE_IS_FAVORITE = "favoriteIsFavorite" + /** A `BrowserSitePermission.key` (`camera`, `microphone`, `location`). */ + const val KEY_SITE_PERMISSION = "sitePermission" + + /** A `BrowserSitePermission.Decision` name (`ASK`, `ALLOW`, `BLOCK`). */ + const val KEY_SITE_DECISION = "siteDecision" + + /** Prefix of the per-permission decision entries in a [MSG_SITE_PERMISSIONS] reply. */ + const val KEY_SITE_PERMISSION_PREFIX = "perm." + /** Boolean: this sandbox surface is now foreground (true) or backgrounded (false). */ const val KEY_FOREGROUND = "foreground" diff --git a/nappletHost/src/main/res/values/strings.xml b/nappletHost/src/main/res/values/strings.xml index 2bc1944e43..2ac71b58ff 100644 --- a/nappletHost/src/main/res/values/strings.xml +++ b/nappletHost/src/main/res/values/strings.xml @@ -37,11 +37,4 @@ <string name="napplet_console_load_error">Failed to load (%1$d): %2$s</string> <string name="napplet_console_http_error">HTTP %1$d %2$s</string> - <!-- Page-originated JavaScript dialogs (alert / confirm / prompt / beforeunload) in the browser --> - <string name="napplet_js_dialog_title">%1$s says</string> - <string name="napplet_js_dialog_title_generic">This page says</string> - <string name="napplet_js_dialog_block">Block dialogs from this page</string> - <string name="napplet_js_leave_title">Leave site?</string> - <string name="napplet_js_leave_message">Changes you made may not be saved.</string> - <string name="napplet_js_leave">Leave</string> </resources> From 810e09e9acde3a218e36452572282c4590c845cc Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 16:15:42 +0000 Subject: [PATCH 07/60] feat(browser): embedded tab chrome, site permissions and home shortcuts - TopControlSheet (Compose) drawn from BrowserChrome, find-in-page bar, Compose JS dialogs, permission prompt and page info for embedded tabs. - WebSitePermissionRegistry: per-origin camera/mic/location answers (same on Tor and open web), editable in Connected Apps detail. - Add to Home screen and favorite launcher shortcuts open the page full screen in Amethyst's browser via WebShortcutActivity, which waits for Tor so a shortcut never falls back to the open web. - 'Open full screen' opens the page on screen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TEkj7Eo2xbidVHAoF8GZKQ --- amethyst/src/main/AndroidManifest.xml | 10 + .../com/vitorpamplona/amethyst/Amethyst.kt | 16 + .../amethyst/favorites/WebShortcutActivity.kt | 80 ++++ .../amethyst/favorites/WebShortcuts.kt | 122 ++++++ .../amethyst/napplet/NappletBrokerService.kt | 43 ++ .../napplet/WebSitePermissionRegistry.kt | 105 +++++ .../loggedIn/browser/EmbeddedPageDialogs.kt | 163 ++++++++ .../loggedIn/browser/EmbeddedPageRequests.kt | 43 ++ .../browser/EmbeddedWebAppController.kt | 144 ++++++- .../screen/loggedIn/browser/WebAppScreen.kt | 210 ++++++++-- .../screen/loggedIn/embed/EmbeddedFindBar.kt | 123 ++++++ .../loggedIn/embed/EmbeddedTabChrome.kt | 36 +- .../screen/loggedIn/embed/EmbeddedTabLayer.kt | 35 ++ .../ui/screen/loggedIn/embed/FindBridge.kt | 43 ++ .../screen/loggedIn/embed/TopControlSheet.kt | 368 ++++++++++++++---- .../loggedIn/favorites/NostrAppScreen.kt | 40 +- .../napplets/ConnectedAppDetailScreen.kt | 77 ++++ 17 files changed, 1515 insertions(+), 143 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/WebShortcutActivity.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/WebShortcuts.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/WebSitePermissionRegistry.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedPageDialogs.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedPageRequests.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedFindBar.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/FindBridge.kt diff --git a/amethyst/src/main/AndroidManifest.xml b/amethyst/src/main/AndroidManifest.xml index c4304d3485..b120f60ad1 100644 --- a/amethyst/src/main/AndroidManifest.xml +++ b/amethyst/src/main/AndroidManifest.xml @@ -665,6 +665,16 @@ android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.Amethyst" /> + <!-- Target of web-app launcher shortcuts (Add to Home screen + favorites). Main process, so the + app and Tor are up before it hands the URL to the full-screen browser; draws nothing. --> + <activity + android:name=".favorites.WebShortcutActivity" + android:exported="false" + android:excludeFromRecents="true" + android:noHistory="true" + android:taskAffinity="" + android:theme="@android:style/Theme.Translucent.NoTitleBar" /> + <!-- Capability-consent dialog. Runs in the main process (the only side trusted to grant). --> <activity android:name=".napplet.NappletConsentActivity" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index c3445013ca..ebe76d1a34 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -23,12 +23,15 @@ package com.vitorpamplona.amethyst import android.app.Application import android.content.ComponentCallbacks2 import android.os.Build +import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.service.http.HttpClientEnvironment import com.vitorpamplona.amethyst.commons.service.http.MediaCallEventListener import com.vitorpamplona.amethyst.favorites.BrowserHistoryRegistry import com.vitorpamplona.amethyst.favorites.BrowserIconRegistry import com.vitorpamplona.amethyst.favorites.FavoriteAppsRegistry +import com.vitorpamplona.amethyst.favorites.WebShortcuts import com.vitorpamplona.amethyst.napplet.WebAppNetworkRegistry +import com.vitorpamplona.amethyst.napplet.WebSitePermissionRegistry import com.vitorpamplona.amethyst.service.logging.Logging import com.vitorpamplona.amethyst.service.nests.AppForegroundRecycleHook import com.vitorpamplona.amethyst.service.okhttp.isEmulator @@ -38,6 +41,8 @@ import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.LogLevel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import java.io.File @@ -145,6 +150,17 @@ class Amethyst : Application() { // Hydrate the device-local favorite-apps list (main process only; the sandbox never reads it). FavoriteAppsRegistry.init(this) + // Mirror web-app favorites into the launcher's long-press shortcuts (open in Amethyst's browser). + CoroutineScope(Dispatchers.Default).launch { + FavoriteAppsRegistry.favorites + .map { apps -> apps.filterIsInstance<FavoriteApp.WebApp>().map { it.url to it.label } } + .distinctUntilChanged() + .collect { WebShortcuts.publishFavorites(this@Amethyst, FavoriteAppsRegistry.favorites.value) } + } + + // Hydrate the per-site camera/microphone/location answers the browser asks about. + WebSitePermissionRegistry.init(this) + // Hydrate the device-local browser visit history (main process only; feeds the omnibox suggestions). BrowserHistoryRegistry.init(this) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/WebShortcutActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/WebShortcutActivity.kt new file mode 100644 index 0000000000..5650f36c6d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/WebShortcutActivity.kt @@ -0,0 +1,80 @@ +/* + * 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.favorites + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.lifecycle.lifecycleScope +import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.tor.TorType +import com.vitorpamplona.amethyst.ui.MainActivity +import com.vitorpamplona.amethyst.ui.tor.TorServiceStatus +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull + +/** + * The target of every web-app launcher shortcut ([WebShortcuts]). Runs in the **main** process, so the app + * (account, broker, Tor) is up before the page opens, then hands off to the full-screen browser through + * [FavoriteAppLauncher.launchUrl] — the same path the Browser tab uses — and finishes without drawing. + * + * On a cold start from the launcher Tor is usually still bootstrapping. Opening straight away would load a + * Tor-routed site over the open web, so when Tor is configured this waits for it; if it doesn't come up in + * time, the user lands in Amethyst (where Tor's state is visible) rather than on the open web. + */ +class WebShortcutActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val url = intent.getStringExtra(EXTRA_URL)?.takeIf { it.startsWith("https://") || it.startsWith("http://") } + if (url == null) { + finish() + return + } + lifecycleScope.launch { + val app = Amethyst.instance + val torWanted = app.torPrefs.torType.value != TorType.OFF && app.torManager.activePortOrNull.value == null + val torReady = + !torWanted || + withTimeoutOrNull(TOR_WAIT_MS) { app.torManager.status.first { it is TorServiceStatus.Active } } != null + if (torReady) { + FavoriteAppLauncher.launchUrl(this@WebShortcutActivity, url) + } else { + startActivity(Intent(this@WebShortcutActivity, MainActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + } + finish() + } + } + + companion object { + private const val EXTRA_URL = "url" + private const val TOR_WAIT_MS = 30_000L + + fun intent( + context: Context, + url: String, + ): Intent = + Intent(context, WebShortcutActivity::class.java) + .setAction(Intent.ACTION_VIEW) + .putExtra(EXTRA_URL, url) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/WebShortcuts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/WebShortcuts.kt new file mode 100644 index 0000000000..b21d8c8755 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/WebShortcuts.kt @@ -0,0 +1,122 @@ +/* + * 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.favorites + +import android.content.Context +import android.graphics.BitmapFactory +import android.widget.Toast +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import androidx.core.graphics.scale +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp +import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.sha256.sha256 +import java.io.File +import com.vitorpamplona.amethyst.commons.R as CommonsR + +/** + * Launcher shortcuts for web apps — the PWA "Add to Home screen" (pinned) and the long-press list on + * Amethyst's own icon (dynamic, from the favorites). Every shortcut opens the page **full screen in + * Amethyst's own browser** through [WebShortcutActivity], never the system browser, so the user's NIP-07 + * signer is there. + * + * Main process only: the icon comes from [BrowserIconRegistry]'s captured favicon when it is a raster + * image, else Amethyst's launcher icon. + */ +object WebShortcuts { + private const val TAG = "WebShortcuts" + private const val MAX_DYNAMIC = 4 + private const val DYNAMIC_PREFIX = "fav:" + private const val ICON_PX = 192 + + /** Asks the launcher to pin a shortcut to [url] labelled [title]. */ + fun requestPin( + context: Context, + url: String, + title: String, + ) { + if (!ShortcutManagerCompat.isRequestPinShortcutSupported(context)) { + Toast.makeText(context, CommonsR.string.browser_home_shortcut_unsupported, Toast.LENGTH_SHORT).show() + return + } + val info = build(context, "web:" + idOf(url), url, title) + runCatching { ShortcutManagerCompat.requestPinShortcut(context, info, null) } + .onFailure { Log.w(TAG, "Pin shortcut request failed", it) } + } + + /** Mirrors the first few web-app favorites into Amethyst's long-press shortcut list. */ + fun publishFavorites( + context: Context, + favorites: List<FavoriteApp>, + ) { + val shortcuts = + favorites + .filterIsInstance<FavoriteApp.WebApp>() + .take(MAX_DYNAMIC) + .map { build(context, DYNAMIC_PREFIX + idOf(it.url), it.url, it.label) } + runCatching { + val stale = + ShortcutManagerCompat + .getDynamicShortcuts(context) + .map { it.id } + .filter { it.startsWith(DYNAMIC_PREFIX) && it !in shortcuts.map { s -> s.id } } + if (stale.isNotEmpty()) ShortcutManagerCompat.removeDynamicShortcuts(context, stale) + shortcuts.forEach { ShortcutManagerCompat.pushDynamicShortcut(context, it) } + }.onFailure { Log.w(TAG, "Could not publish favorite shortcuts", it) } + } + + private fun build( + context: Context, + id: String, + url: String, + title: String, + ): ShortcutInfoCompat { + val label = title.ifBlank { BrowserChrome.displayHost(url) } + return ShortcutInfoCompat + .Builder(context, id) + .setShortLabel(label.take(24)) + .setLongLabel(label.take(48)) + .setIcon(iconFor(context, url)) + .setIntent(WebShortcutActivity.intent(context, url)) + .build() + } + + private fun iconFor( + context: Context, + url: String, + ): IconCompat { + val bitmap = + BrowserIconRegistry + .iconModelFor(BrowserChrome.displayHost(url)) + ?.removePrefix("file://") + ?.let { path -> runCatching { BitmapFactory.decodeFile(File(path).absolutePath) }.getOrNull() } + return if (bitmap != null) { + IconCompat.createWithBitmap(if (bitmap.width < ICON_PX) bitmap.scale(ICON_PX, ICON_PX, filter = false) else bitmap) + } else { + IconCompat.createWithResource(context, R.mipmap.ic_launcher) + } + } + + private fun idOf(url: String): String = sha256(url.encodeToByteArray()).take(12).joinToString("") { "%02x".format(it) } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt index ed067f5c1a..fdf5493f62 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt @@ -33,6 +33,7 @@ import android.os.RemoteException import android.os.SystemClock import androidx.core.net.toUri import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionLedger import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.napplet.NappletBroker @@ -45,6 +46,7 @@ import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletResponse import com.vitorpamplona.amethyst.favorites.BrowserHistoryRegistry import com.vitorpamplona.amethyst.favorites.BrowserIconRegistry import com.vitorpamplona.amethyst.favorites.FavoriteAppsRegistry +import com.vitorpamplona.amethyst.favorites.WebShortcuts import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.napplet.gateways.AccountNappletGateways import com.vitorpamplona.amethyst.napplethost.NappletIpc @@ -249,6 +251,47 @@ class NappletBrokerService : Service() { return true } + // The browser asks what the user already answered for a site's camera/microphone/location. + if (msg.what == NappletIpc.MSG_QUERY_SITE_PERMISSIONS) { + val replyTo = msg.replyTo ?: return true + val data = msg.data ?: return true + val origin = data.getString(NappletIpc.KEY_BROWSER_ORIGIN)?.takeIf { it.isNotBlank() } ?: return true + WebSitePermissionRegistry.init(applicationContext) + val reply = + Message.obtain(null, NappletIpc.MSG_SITE_PERMISSIONS).apply { + this.data = + Bundle().apply { + putLong(NappletIpc.KEY_REQUEST_ID, data.getLong(NappletIpc.KEY_REQUEST_ID)) + putString(NappletIpc.KEY_BROWSER_ORIGIN, origin) + BrowserSitePermission.entries.forEach { permission -> + putString(NappletIpc.KEY_SITE_PERMISSION_PREFIX + permission.key, WebSitePermissionRegistry.decision(origin, permission).name) + } + } + } + runCatching { replyTo.send(reply) } + return true + } + + // The browser relays the user's answer to a site's permission prompt; remember it per origin. + if (msg.what == NappletIpc.MSG_SET_SITE_PERMISSION) { + val data = msg.data ?: return true + val origin = data.getString(NappletIpc.KEY_BROWSER_ORIGIN)?.takeIf { it.isNotBlank() } ?: return true + val permission = BrowserSitePermission.fromKey(data.getString(NappletIpc.KEY_SITE_PERMISSION)) ?: return true + val decision = runCatching { BrowserSitePermission.Decision.valueOf(data.getString(NappletIpc.KEY_SITE_DECISION).orEmpty()) }.getOrNull() ?: return true + WebSitePermissionRegistry.init(applicationContext) + WebSitePermissionRegistry.set(origin, permission, decision) + return true + } + + // The full-screen browser's "Add to Home screen": pin a shortcut that reopens it in Amethyst. + if (msg.what == NappletIpc.MSG_ADD_TO_HOME_SCREEN) { + val data = msg.data ?: return true + val url = data.getString(NappletIpc.KEY_FAVORITE_URL)?.takeIf { it.startsWith("https://") || it.startsWith("http://") } ?: return true + BrowserIconRegistry.init(applicationContext) + WebShortcuts.requestPin(applicationContext, url, data.getString(NappletIpc.KEY_FAVORITE_LABEL).orEmpty()) + return true + } + // The direct-WebView browser relays its per-host Tor choice; persist it (main process only). if (msg.what == NappletIpc.MSG_SET_WEB_TOR) { val data = msg.data ?: return true diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/WebSitePermissionRegistry.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/WebSitePermissionRegistry.kt new file mode 100644 index 0000000000..655c527a1d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/WebSitePermissionRegistry.kt @@ -0,0 +1,105 @@ +/* + * 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.napplet + +import android.content.Context +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission +import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission.Decision +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +private val Context.webSitePermissionDataStore by preferencesDataStore(name = "web_site_permissions") + +/** + * The user's answers to web sites' camera / microphone / location requests, per **origin** + * (`https://example.com`), the way Chrome's site settings keep them. Absent = [Decision.ASK]: the browser + * prompts the next time the site asks. The same answer holds on Tor and the open web — routing never + * changes it. + * + * Lives only in the **main process**. The keyless browser reads and writes it through the broker + * ([com.vitorpamplona.amethyst.napplethost.NappletIpc.MSG_QUERY_SITE_PERMISSIONS] / + * [com.vitorpamplona.amethyst.napplethost.NappletIpc.MSG_SET_SITE_PERMISSION]); the Connected Apps detail + * screen shows and resets it. In-memory state is authoritative for the session, written through to a + * DataStore stored under `"<origin>|<permission>"` keys. + */ +object WebSitePermissionRegistry { + private val _decisions = MutableStateFlow<Map<String, Map<BrowserSitePermission, Decision>>>(emptyMap()) + + /** origin → permission → decision (only answered permissions appear). */ + val decisions: StateFlow<Map<String, Map<BrowserSitePermission, Decision>>> = _decisions.asStateFlow() + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + @Volatile private var appContext: Context? = null + + fun init(context: Context) { + if (appContext != null) return + val ctx = context.applicationContext + appContext = ctx + scope.launch { + val loaded = mutableMapOf<String, MutableMap<BrowserSitePermission, Decision>>() + ctx.webSitePermissionDataStore.data.first().asMap().forEach { (key, value) -> + val origin = key.name.substringBeforeLast('|') + val permission = BrowserSitePermission.fromKey(key.name.substringAfterLast('|')) ?: return@forEach + val decision = runCatching { Decision.valueOf(value.toString()) }.getOrNull() ?: return@forEach + loaded.getOrPut(origin) { mutableMapOf() }[permission] = decision + } + // Answers given this session (before hydration finished) win over the disk copy. + _decisions.update { current -> + (loaded.keys + current.keys).associateWith { origin -> loaded[origin].orEmpty() + current[origin].orEmpty() } + } + } + } + + fun decision( + origin: String, + permission: BrowserSitePermission, + ): Decision = _decisions.value[origin]?.get(permission) ?: Decision.ASK + + fun set( + origin: String, + permission: BrowserSitePermission, + decision: Decision, + ) { + _decisions.update { current -> + val forOrigin = current[origin].orEmpty().toMutableMap() + if (decision == Decision.ASK) forOrigin.remove(permission) else forOrigin[permission] = decision + if (forOrigin.isEmpty()) current - origin else current + (origin to forOrigin) + } + val ctx = appContext ?: return + scope.launch { + ctx.webSitePermissionDataStore.edit { prefs -> + val key = stringPreferencesKey("$origin|${permission.key}") + if (decision == Decision.ASK) prefs.remove(key) else prefs[key] = decision.name + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedPageDialogs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedPageDialogs.kt new file mode 100644 index 0000000000..e7b4c6821e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedPageDialogs.kt @@ -0,0 +1,163 @@ +/* + * 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.browser + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.commons.R as CommonsR + +/** + * An embedded page's `alert` / `confirm` / `prompt` / `beforeunload`, drawn by the main process over the tab + * (the provider's WebView has no window to draw one). Titled with the page's host ("example.com says") so a + * page can't pass its dialog off as Amethyst's own, and offering "Block dialogs from this page" from the + * page's second dialog on — the same rules as the full-screen browser. + */ +@Composable +fun EmbeddedJsDialogView( + dialog: EmbeddedJsDialog, + onAnswer: (confirmed: Boolean, text: String?, block: Boolean) -> Unit, +) { + var text by remember(dialog.id) { mutableStateOf(dialog.defaultValue) } + val leave = dialog.type == EmbeddedJsDialog.Type.BEFORE_UNLOAD + val host = dialog.url?.let { BrowserChrome.originOf(it) }?.let(BrowserChrome::displayHost) + AlertDialog( + onDismissRequest = { onAnswer(false, null, false) }, + title = { + Text( + when { + leave -> stringRes(CommonsR.string.browser_js_leave_title) + host != null -> stringRes(CommonsR.string.browser_js_dialog_title, host) + else -> stringRes(CommonsR.string.browser_js_dialog_title_generic) + }, + ) + }, + text = { + Column(Modifier.verticalScroll(rememberScrollState())) { + // The page's own beforeunload text is ignored, as in every current browser (it was abused). + Text(if (leave) stringRes(CommonsR.string.browser_js_leave_message) else dialog.message) + if (dialog.type == EmbeddedJsDialog.Type.PROMPT) { + Spacer(Modifier.height(12.dp)) + OutlinedTextField(value = text, onValueChange = { text = it }, singleLine = true, modifier = Modifier.fillMaxWidth()) + } + } + }, + confirmButton = { + TextButton(onClick = { onAnswer(true, if (dialog.type == EmbeddedJsDialog.Type.PROMPT) text else null, false) }) { + Text(if (leave) stringRes(CommonsR.string.browser_js_leave) else stringRes(android.R.string.ok)) + } + }, + dismissButton = { + Row { + if (dialog.offerBlock) { + // A blocked page may not keep the user on it, so blocking a beforeunload means leaving. + TextButton(onClick = { onAnswer(leave, null, true) }) { Text(stringRes(CommonsR.string.browser_js_dialog_block)) } + } + if (dialog.type != EmbeddedJsDialog.Type.ALERT) { + TextButton(onClick = { onAnswer(false, null, false) }) { Text(stringRes(android.R.string.cancel)) } + } + } + }, + ) +} + +/** + * Chrome's permission bubble for an embedded page: "<host> wants to — use your camera — Block / Allow". + * Dismissing it denies this once without remembering anything. + */ +@Composable +fun EmbeddedPermissionPrompt( + origin: String, + permissions: Set<BrowserSitePermission>, + onAnswer: (allow: Boolean?) -> Unit, +) { + AlertDialog( + onDismissRequest = { onAnswer(null) }, + title = { Text(stringRes(CommonsR.string.browser_permission_title, BrowserChrome.displayHost(origin))) }, + text = { + Column { + permissions.forEach { permission -> + Text( + "• " + + stringRes( + when (permission) { + BrowserSitePermission.CAMERA -> CommonsR.string.browser_permission_camera + BrowserSitePermission.MICROPHONE -> CommonsR.string.browser_permission_microphone + BrowserSitePermission.LOCATION -> CommonsR.string.browser_permission_location + }, + ), + ) + } + } + }, + confirmButton = { TextButton(onClick = { onAnswer(true) }) { Text(stringRes(CommonsR.string.browser_permission_allow)) } }, + dismissButton = { TextButton(onClick = { onAnswer(false) }) { Text(stringRes(CommonsR.string.browser_permission_block)) } }, + ) +} + +/** Chrome's page-info sheet for an embedded page: connection, Tor, certificate; site settings; clear data. */ +@Composable +fun EmbeddedPageInfoDialog( + host: String, + info: String?, + onPermissions: (() -> Unit)?, + onClearData: () -> Unit, + onDismiss: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(host) }, + text = { Text(info ?: "…") }, + confirmButton = { TextButton(onClick = onDismiss) { Text(stringRes(android.R.string.ok)) } }, + dismissButton = { + Row { + TextButton(onClick = { + onClearData() + onDismiss() + }) { Text(stringRes(CommonsR.string.browser_page_info_clear_data)) } + if (onPermissions != null) { + TextButton(onClick = { + onPermissions() + onDismiss() + }) { Text(stringRes(CommonsR.string.browser_page_info_permissions)) } + } + } + }, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedPageRequests.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedPageRequests.kt new file mode 100644 index 0000000000..90d9ff904b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedPageRequests.kt @@ -0,0 +1,43 @@ +/* + * 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.browser + +import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission + +/** A JS dialog an embedded page opened, waiting for the user (the page's script is paused meanwhile). */ +data class EmbeddedJsDialog( + val id: Long, + val type: Type, + val url: String?, + val message: String, + val defaultValue: String, + /** Offer "Block dialogs from this page" (from the page's second dialog on, as Chrome does). */ + val offerBlock: Boolean, +) { + enum class Type { ALERT, CONFIRM, PROMPT, BEFORE_UNLOAD } +} + +/** A camera / microphone / location request from an embedded page, waiting for an answer. */ +data class EmbeddedPermissionRequest( + val id: Long, + val origin: String, + val permissions: Set<BrowserSitePermission>, +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt index 3af22b5bbc..427b3376d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt @@ -35,10 +35,13 @@ import android.os.Message import android.os.Messenger import android.os.SystemClock import androidx.annotation.RequiresApi +import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory import androidx.privacysandbox.ui.client.view.SandboxedSdkView import androidx.privacysandbox.ui.core.SandboxedUiAdapter +import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission import com.vitorpamplona.amethyst.napplet.NappletWebViewProfiles import com.vitorpamplona.amethyst.napplet.WebFileChooserCoordinator import com.vitorpamplona.amethyst.napplethost.NappletBrowserContract @@ -48,6 +51,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedImeBridge import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedLoadStatus import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedMagnifierProbe import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedSurfaceController +import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.FindBridge +import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.FindResult import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.ImeEvent import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.MagnifierFrame import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.parseImeEvent @@ -69,7 +74,8 @@ class EmbeddedWebAppController( ) : EmbeddedSurfaceController, EmbeddedImeBridge, EmbeddedMagnifierProbe, - ConsoleBridge { + ConsoleBridge, + FindBridge { private val incoming = Messenger(Handler(Looper.getMainLooper(), ::onServiceMessage)) private var serviceMessenger: Messenger? = null private var bound = false @@ -106,8 +112,28 @@ class EmbeddedWebAppController( // previous view can never reap the replacement. private var sessionId: String = "browser-${SESSION_SEQ.incrementAndGet()}" - /** Invoked on the main thread when the page navigates or retitles: (url, title or null, canGoBack). */ - var onUrlChanged: ((String, String?, Boolean) -> Unit)? = null + /** Invoked on the main thread when the page navigates or retitles: (url, title or null, canGoBack, canGoForward). */ + var onUrlChanged: ((String, String?, Boolean, Boolean) -> Unit)? = null + + // ---- page-initiated UI, drawn by the main process (the provider has no window) ---- + + private val _findResult = mutableStateOf<FindResult?>(null) + override val findResult: State<FindResult?> = _findResult + + /** The JS dialog the page is waiting on, if any. */ + val pendingDialog = mutableStateOf<EmbeddedJsDialog?>(null) + + /** The camera / microphone / location request the page is waiting on, if any. */ + val pendingPermission = mutableStateOf<EmbeddedPermissionRequest?>(null) + + /** Page-info text for the page on screen, once requested. */ + val pageInfo = mutableStateOf<String?>(null) + + /** A main-frame load is in flight (the pill's reload button becomes stop). */ + val isLoading = mutableStateOf(false) + + /** The page is showing HTML fullscreen (a video) inside the surface. */ + val isFullscreen = mutableStateOf(false) override var onImeEvent: ((ImeEvent) -> Unit)? = null @@ -150,6 +176,9 @@ class EmbeddedWebAppController( onMagnifierFrame = null onLoadStatusChanged = null consoleLogs.clear() + pendingDialog.value = null + pendingPermission.value = null + isFullscreen.value = false } override fun teardown() = unbind() @@ -235,8 +264,9 @@ class EmbeddedWebAppController( NappletBrowserContract.MSG_URL_CHANGED -> { val url = msg.data?.getString(NappletBrowserContract.KEY_URL).orEmpty() val canGoBack = msg.data?.getBoolean(NappletBrowserContract.KEY_CAN_GO_BACK, false) ?: false + val canGoForward = msg.data?.getBoolean(NappletBrowserContract.KEY_CAN_GO_FORWARD, false) ?: false val title = msg.data?.getString(NappletBrowserContract.KEY_TITLE) - onUrlChanged?.invoke(url, title, canGoBack) + onUrlChanged?.invoke(url, title, canGoBack, canGoForward) } NappletBrowserContract.MSG_IME_EVENT -> { val payload = msg.data?.getString(NappletBrowserContract.KEY_IME_PAYLOAD) ?: return true @@ -275,6 +305,56 @@ class EmbeddedWebAppController( } } } + NappletBrowserContract.MSG_FIND_RESULT -> { + val data = msg.data ?: return true + _findResult.value = FindResult(data.getInt(NappletBrowserContract.KEY_FIND_ACTIVE), data.getInt(NappletBrowserContract.KEY_FIND_TOTAL)) + } + NappletBrowserContract.MSG_PAGE_INFO -> pageInfo.value = msg.data?.getString(NappletBrowserContract.KEY_PAGE_INFO) + NappletBrowserContract.MSG_JS_DIALOG -> { + val data = msg.data ?: return true + val id = data.getLong(NappletBrowserContract.KEY_DIALOG_ID) + // One page, one dialog at a time; if another is somehow still up, the newcomer is refused. + if (pendingDialog.value != null) { + answerDialog(id, confirmed = false) + return true + } + pendingDialog.value = + EmbeddedJsDialog( + id = id, + type = + when (data.getString(NappletBrowserContract.KEY_DIALOG_TYPE)) { + "confirm" -> EmbeddedJsDialog.Type.CONFIRM + "prompt" -> EmbeddedJsDialog.Type.PROMPT + "beforeunload" -> EmbeddedJsDialog.Type.BEFORE_UNLOAD + else -> EmbeddedJsDialog.Type.ALERT + }, + url = data.getString(NappletBrowserContract.KEY_URL), + message = data.getString(NappletBrowserContract.KEY_DIALOG_MESSAGE).orEmpty(), + defaultValue = data.getString(NappletBrowserContract.KEY_DIALOG_DEFAULT).orEmpty(), + offerBlock = data.getBoolean(NappletBrowserContract.KEY_DIALOG_OFFER_BLOCK, false), + ) + } + NappletBrowserContract.MSG_PERMISSION_REQUEST -> { + val data = msg.data ?: return true + val id = data.getLong(NappletBrowserContract.KEY_PERMISSION_ID) + val origin = data.getString(NappletBrowserContract.KEY_BROWSER_ORIGIN) + val wanted = + data + .getStringArray(NappletBrowserContract.KEY_PERMISSIONS) + .orEmpty() + .mapNotNull(BrowserSitePermission::fromKey) + .toSet() + if (origin == null || wanted.isEmpty() || pendingPermission.value != null) { + answerPermission(id, emptySet()) + } else { + pendingPermission.value = EmbeddedPermissionRequest(id, origin, wanted) + } + } + NappletBrowserContract.MSG_PERMISSION_CANCEL -> { + val id = msg.data?.getLong(NappletBrowserContract.KEY_PERMISSION_ID) + if (pendingPermission.value?.id == id) pendingPermission.value = null + } + NappletBrowserContract.MSG_FULLSCREEN -> isFullscreen.value = msg.data?.getBoolean(NappletBrowserContract.KEY_ENABLED, false) ?: false NappletBrowserContract.MSG_MAGNIFIER_FRAME -> { val data = msg.data ?: return true val bytes = data.getByteArray(NappletBrowserContract.KEY_MAG_BYTES) ?: return true @@ -330,6 +410,7 @@ class EmbeddedWebAppController( private fun publishLoadStatus(status: EmbeddedLoadStatus) { loadStatus = status + isLoading.value = status.isLoading onLoadStatusChanged?.invoke(status) } @@ -337,6 +418,61 @@ class EmbeddedWebAppController( fun back() = send(NappletBrowserContract.MSG_BACK) {} + fun forward() = send(NappletBrowserContract.MSG_FORWARD) {} + + fun stop() = send(NappletBrowserContract.MSG_STOP) {} + + override fun find(query: String) { + if (query.isEmpty()) _findResult.value = null + send(NappletBrowserContract.MSG_FIND) { putString(NappletBrowserContract.KEY_FIND_QUERY, query) } + } + + override fun findNext(forward: Boolean) = send(NappletBrowserContract.MSG_FIND_NEXT) { putBoolean(NappletBrowserContract.KEY_FIND_FORWARD, forward) } + + fun setDesktopSite(enabled: Boolean) = send(NappletBrowserContract.MSG_SET_DESKTOP) { putBoolean(NappletBrowserContract.KEY_ENABLED, enabled) } + + fun setTextZoom(percent: Int) = send(NappletBrowserContract.MSG_SET_TEXT_ZOOM) { putInt(NappletBrowserContract.KEY_TEXT_ZOOM, percent) } + + /** Back to the app's home origin ([homeUrl]), Chrome's out-of-scope ✕. */ + fun backToScope(homeUrl: String) = send(NappletBrowserContract.MSG_BACK_TO_SCOPE) { putString(NappletBrowserContract.KEY_URL, homeUrl) } + + fun clearSiteData() = send(NappletBrowserContract.MSG_CLEAR_SITE_DATA) {} + + fun requestPageInfo() { + pageInfo.value = null + send(NappletBrowserContract.MSG_PAGE_INFO_REQUEST) {} + } + + fun exitFullscreen() = send(NappletBrowserContract.MSG_EXIT_FULLSCREEN) {} + + /** Answers the page's JS dialog [id]; [block] suppresses its further dialogs until it navigates. */ + fun answerDialog( + id: Long, + confirmed: Boolean, + text: String? = null, + block: Boolean = false, + ) { + if (pendingDialog.value?.id == id) pendingDialog.value = null + send(NappletBrowserContract.MSG_JS_DIALOG_RESULT) { + putLong(NappletBrowserContract.KEY_DIALOG_ID, id) + putBoolean(NappletBrowserContract.KEY_DIALOG_CONFIRMED, confirmed) + text?.let { putString(NappletBrowserContract.KEY_DIALOG_TEXT, it) } + putBoolean(NappletBrowserContract.KEY_DIALOG_BLOCK, block) + } + } + + /** Grants [granted] (possibly nothing) for the page's permission request [id]. */ + fun answerPermission( + id: Long, + granted: Set<BrowserSitePermission>, + ) { + if (pendingPermission.value?.id == id) pendingPermission.value = null + send(NappletBrowserContract.MSG_PERMISSION_RESULT) { + putLong(NappletBrowserContract.KEY_PERMISSION_ID, id) + putStringArray(NappletBrowserContract.KEY_PERMISSIONS, granted.map { it.key }.toTypedArray()) + } + } + fun setTor(useTor: Boolean) = send(NappletBrowserContract.MSG_SET_TOR) { putBoolean(NappletBrowserContract.KEY_USE_TOR, useTor) } override fun sendImeOp(json: String) = send(NappletBrowserContract.MSG_IME_OP) { putString(NappletBrowserContract.KEY_IME_PAYLOAD, json) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/WebAppScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/WebAppScreen.kt index 4c67b7334c..4c26663a39 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/WebAppScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/WebAppScreen.kt @@ -20,8 +20,13 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.browser +import android.Manifest +import android.content.pm.PackageManager import android.os.Build +import android.widget.Toast import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -31,8 +36,10 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -42,10 +49,14 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext +import androidx.core.content.ContextCompat import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission +import com.vitorpamplona.amethyst.commons.browser.OmniboxInput import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.navigation.favoriteIds @@ -55,12 +66,16 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher import com.vitorpamplona.amethyst.favorites.FavoriteAppsRegistry +import com.vitorpamplona.amethyst.favorites.WebShortcuts import com.vitorpamplona.amethyst.napplet.WebAppNetworkRegistry +import com.vitorpamplona.amethyst.napplet.WebSitePermissionRegistry +import com.vitorpamplona.amethyst.napplethost.BrowserWebTools import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabChrome import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabFactory import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabHost +import com.vitorpamplona.amethyst.commons.R as CommonsR /** * A **Web app** (a Nostr web client, reached by [url]) rendered as an **in-app tab** — for *any* URL, @@ -106,6 +121,10 @@ private fun EmbeddedWebAppTab( // The page's own <title>; null until the current document reports one (the sheet shows the host). var pageTitle by remember { mutableStateOf<String?>(null) } var canGoBack by remember { mutableStateOf(false) } + var canGoForward by remember { mutableStateOf(false) } + var desktopSite by remember { mutableStateOf(false) } + var textZoom by remember { mutableIntStateOf(BrowserChrome.DEFAULT_TEXT_ZOOM) } + var showPageInfo by remember { mutableStateOf(false) } val proxyAvailable = remember { Amethyst.instance.torManager.activePortOrNull.value != null } // Start from this site's remembered Tor choice (some sites' servers reject Tor exits, so the user @@ -123,48 +142,101 @@ private fun EmbeddedWebAppTab( remember(id, EmbeddedTabHost.rebuildEpoch) { EmbeddedTabFactory.acquireWebApp(context, url, backgroundColor) } + val isLoading by controller.isLoading // Keep the URL/back callback fresh (cheap, needs the latest closure). SideEffect { - controller.onUrlChanged = { newUrl, title, back -> + controller.onUrlChanged = { newUrl, title, back, forward -> if (newUrl != "about:blank") { currentUrl = newUrl pageTitle = title } canGoBack = back + canGoForward = forward } } + fun toggleFavorite() { + val favId = "url:$currentUrl" + if (FavoriteAppsRegistry.isFavorite(favId)) { + FavoriteAppsRegistry.remove(favId) + } else { + FavoriteAppsRegistry.add(FavoriteApp.WebApp(currentUrl, pageTitle ?: hostLabel(currentUrl), System.currentTimeMillis())) + } + } + + // NIP-07 grants for a plain web client are keyed per visited origin as `browser:<origin>` (see + // NappletBrokerService.BROWSER_IDENTITY_AUTHOR); site settings jump straight to that detail screen. + fun openSiteSettings() { + browserOrigin(currentUrl)?.let { origin -> nav.nav(Route.ConnectedAppDetail("browser:$origin")) } + } + // Rebuilt only when a displayed value changes, so the tab layer isn't recomposed every frame. val chrome = - remember(currentUrl, pageTitle, torOn, proxyAvailable, isFavorite, controller) { + remember(currentUrl, pageTitle, canGoBack, canGoForward, isLoading, torOn, proxyAvailable, isFavorite, desktopSite, textZoom, controller) { EmbeddedTabChrome( title = pageTitle ?: hostLabel(currentUrl), - isSandbox = false, - onReload = { controller.reload() }, - // The page the user is looking at, not the one the tab was pinned with. - onOpenFull = { FavoriteAppLauncher.launchUrl(context, currentUrl) }, - torOn = if (proxyAvailable) torOn else null, - onToggleTor = { - torOn = !torOn - controller.setTor(torOn) - WebAppNetworkRegistry.set(url, torOn) - }, + state = + BrowserChrome.State( + surface = BrowserChrome.Surface.WEB, + presentation = BrowserChrome.Presentation.EMBEDDED, + url = currentUrl, + startUrl = url, + canGoBack = canGoBack, + canGoForward = canGoForward, + isLoading = isLoading, + torOn = if (proxyAvailable) torOn else null, + hasSiteSettings = browserOrigin(currentUrl) != null, + ), isFavorite = isFavorite, - onFavorite = { - val favId = "url:$currentUrl" - if (FavoriteAppsRegistry.isFavorite(favId)) { - FavoriteAppsRegistry.remove(favId) - } else { - FavoriteAppsRegistry.add(FavoriteApp.WebApp(currentUrl, hostLabel(currentUrl), System.currentTimeMillis())) + desktopSite = desktopSite, + textZoom = textZoom, + onAction = { action -> + when (action) { + BrowserChrome.Action.BACK -> controller.back() + BrowserChrome.Action.FORWARD -> controller.forward() + BrowserChrome.Action.RELOAD -> controller.reload() + BrowserChrome.Action.STOP -> controller.stop() + BrowserChrome.Action.FAVORITE -> toggleFavorite() + BrowserChrome.Action.SHARE -> BrowserWebTools.share(context, pageTitle, null, currentUrl) + BrowserChrome.Action.BACK_TO_APP -> controller.backToScope(url) + BrowserChrome.Action.COPY_LINK -> BrowserWebTools.copyToClipboard(context, currentUrl) + BrowserChrome.Action.DESKTOP_SITE -> { + desktopSite = !desktopSite + controller.setDesktopSite(desktopSite) + } + BrowserChrome.Action.ADD_TO_HOME_SCREEN -> WebShortcuts.requestPin(context, currentUrl, pageTitle ?: hostLabel(currentUrl)) + BrowserChrome.Action.OPEN_IN_BROWSER_APP -> BrowserWebTools.openInOtherBrowser(context, currentUrl) + // The page the user is looking at, not the one the tab was pinned with. + BrowserChrome.Action.OPEN_FULL_SCREEN -> FavoriteAppLauncher.launchUrl(context, currentUrl) + BrowserChrome.Action.TOR -> { + torOn = !torOn + controller.setTor(torOn) + WebAppNetworkRegistry.set(currentUrl, torOn) + } + BrowserChrome.Action.SITE_SETTINGS -> openSiteSettings() + else -> Unit } }, - // NIP-07 grants for a plain web client are keyed per visited origin as `browser:<origin>` - // (see NappletBrokerService.BROWSER_IDENTITY_AUTHOR); jump straight to that detail screen. - onPermissions = - browserOrigin(currentUrl)?.let { origin -> - { nav.nav(Route.ConnectedAppDetail("browser:$origin")) } - }, + onNavigate = { text -> + val resolved = OmniboxInput.resolve(text) + if (resolved != null) { + // .onion only resolves over Tor. + if (resolved.forceTor && proxyAvailable && !torOn) { + torOn = true + controller.setTor(true) + } + controller.navigate(resolved.url) + } + }, + onTextZoom = { percent -> + textZoom = percent + controller.setTextZoom(percent) + }, + onOriginTap = { + controller.requestPageInfo() + showPageInfo = true + }, ) } // Publish the top-sheet controls to the tab layer (which draws them over the z-below surface). In a @@ -182,7 +254,12 @@ private fun EmbeddedWebAppTab( } } - BackHandler(enabled = canGoBack) { controller.back() } + val isFullscreen by controller.isFullscreen + BackHandler(enabled = canGoBack && !isFullscreen) { controller.back() } + // A fullscreen video inside the tab: back leaves fullscreen first, as in Chrome. + BackHandler(enabled = isFullscreen) { controller.exitFullscreen() } + + EmbeddedPageUi(controller, currentUrl, showPageInfo, onPageInfoDismiss = { showPageInfo = false }, onSiteSettings = ::openSiteSettings) Scaffold( bottomBar = { @@ -200,6 +277,89 @@ private fun EmbeddedWebAppTab( } } +/** + * Everything an embedded page asks the user for, drawn by the main process because the provider has no + * window: JS dialogs, camera / microphone / location prompts (remembered per origin in + * [WebSitePermissionRegistry], then Android's own runtime permission), and page info. + */ +@RequiresApi(Build.VERSION_CODES.R) +@Composable +private fun EmbeddedPageUi( + controller: EmbeddedWebAppController, + currentUrl: String, + showPageInfo: Boolean, + onPageInfoDismiss: () -> Unit, + onSiteSettings: () -> Unit, +) { + val context = LocalContext.current + + val dialog by controller.pendingDialog + dialog?.let { d -> + EmbeddedJsDialogView(d) { confirmed, text, block -> controller.answerDialog(d.id, confirmed, text, block) } + } + + // Android's runtime permission, asked only for what the user allowed the site to use. + var runtimeRequest by remember { mutableStateOf<Pair<Long, Set<BrowserSitePermission>>?>(null) } + val runtimeLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { + val (requestId, allowed) = runtimeRequest ?: return@rememberLauncherForActivityResult + runtimeRequest = null + val granted = allowed.filter { ContextCompat.checkSelfPermission(context, androidPermissionFor(it)) == PackageManager.PERMISSION_GRANTED }.toSet() + if (granted.size < allowed.size) Toast.makeText(context, CommonsR.string.browser_permission_system_denied, Toast.LENGTH_LONG).show() + controller.answerPermission(requestId, granted) + } + + fun grant( + requestId: Long, + allowed: Set<BrowserSitePermission>, + ) { + val missing = allowed.map(::androidPermissionFor).filter { ContextCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED } + if (missing.isEmpty() || runtimeRequest != null) { + controller.answerPermission(requestId, if (missing.isEmpty()) allowed else emptySet()) + } else { + runtimeRequest = requestId to allowed + runtimeLauncher.launch(missing.toTypedArray()) + } + } + + val permissionRequest by controller.pendingPermission + permissionRequest?.let { request -> + val decisions = remember(request.id) { request.permissions.associateWith { WebSitePermissionRegistry.decision(request.origin, it) } } + val allowed = decisions.filterValues { it == BrowserSitePermission.Decision.ALLOW }.keys + val ask = decisions.filterValues { it == BrowserSitePermission.Decision.ASK }.keys + if (ask.isEmpty()) { + LaunchedEffect(request.id) { grant(request.id, allowed) } + } else { + EmbeddedPermissionPrompt(request.origin, ask) { allow -> + // null = dismissed without an answer: deny this once, remember nothing. + if (allow != null) { + val decision = if (allow) BrowserSitePermission.Decision.ALLOW else BrowserSitePermission.Decision.BLOCK + ask.forEach { WebSitePermissionRegistry.set(request.origin, it, decision) } + } + grant(request.id, if (allow == true) allowed + ask else allowed) + } + } + } + + if (showPageInfo) { + val info by controller.pageInfo + EmbeddedPageInfoDialog( + host = hostLabel(currentUrl), + info = info, + onPermissions = if (browserOrigin(currentUrl) != null) onSiteSettings else null, + onClearData = { controller.clearSiteData() }, + onDismiss = onPageInfoDismiss, + ) + } +} + +private fun androidPermissionFor(permission: BrowserSitePermission): String = + when (permission) { + BrowserSitePermission.CAMERA -> Manifest.permission.CAMERA + BrowserSitePermission.MICROPHONE -> Manifest.permission.RECORD_AUDIO + BrowserSitePermission.LOCATION -> Manifest.permission.ACCESS_COARSE_LOCATION + } + /** The host of [url] for the tab title, falling back to the raw string. */ internal fun hostLabel(url: String): String = runCatching { url.toUri().host }.getOrNull()?.takeIf { it.isNotBlank() } ?: url diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedFindBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedFindBar.kt new file mode 100644 index 0000000000..6d2492c951 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedFindBar.kt @@ -0,0 +1,123 @@ +/* + * 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.embed + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.commons.R as CommonsR + +/** + * Find in page for the active embedded tab, docked at the bottom of its bounds (the top edge belongs to the + * pill's grabber). Mirrors the full-screen browser's native find bar: query, "n/m", previous, next, close. + */ +@Composable +fun EmbeddedFindBar( + bridge: FindBridge, + onClose: () -> Unit, + modifier: Modifier = Modifier, +) { + var query by remember { mutableStateOf("") } + val result by bridge.findResult + val focus = remember { FocusRequester() } + LaunchedEffect(Unit) { focus.requestFocus() } + + fun close() { + bridge.find("") + onClose() + } + + Box(modifier, contentAlignment = Alignment.BottomCenter) { + Surface( + color = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + tonalElevation = 0.dp, + shadowElevation = 6.dp, + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + ) { + Row(Modifier.fillMaxWidth().padding(start = 4.dp, end = 4.dp), verticalAlignment = Alignment.CenterVertically) { + TextField( + value = query, + onValueChange = { + query = it + bridge.find(it) + }, + modifier = Modifier.weight(1f).focusRequester(focus), + singleLine = true, + placeholder = { Text(stringRes(CommonsR.string.browser_find_hint)) }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions(onSearch = { bridge.findNext(true) }), + colors = + TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + ) + val counts = result + if (query.isNotEmpty() && counts != null) { + Text( + if (counts.total > 0) stringRes(CommonsR.string.browser_find_count, counts.active + 1, counts.total) else "0/0", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 4.dp), + ) + } + IconButton(onClick = { bridge.findNext(false) }) { + Icon(MaterialSymbols.KeyboardArrowUp, contentDescription = stringRes(CommonsR.string.browser_find_previous)) + } + IconButton(onClick = { bridge.findNext(true) }) { + Icon(MaterialSymbols.KeyboardArrowDown, contentDescription = stringRes(CommonsR.string.browser_find_next)) + } + IconButton(onClick = ::close) { + Icon(MaterialSymbols.Close, contentDescription = stringRes(CommonsR.string.browser_find_close)) + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabChrome.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabChrome.kt index fc28e0cf7d..5e8817bbb4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabChrome.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabChrome.kt @@ -20,30 +20,28 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.embed +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome + /** - * The controls a running app surface offers through its top pull-down sheet — described as plain data so + * The controls a running app surface offers through its top pull-down pill — described as plain data so * [EmbeddedTabLayer] can draw the sheet over the (z-below) surface for the active tab. Deliberately not a * corner pill: that's where a site usually puts the user's own avatar/menu, so the handle lives at the - * top-center instead and only the actions the surface actually supports are shown. + * top-center instead. + * + * *Which* actions show, and in what order, comes from [BrowserChrome] with [state] — the same layout the + * full-screen browser's native sheet uses — so the two can't drift. Everything the user picks arrives in + * [onAction]; the console and find-in-page rows are handled by the layer itself. */ data class EmbeddedTabChrome( val title: String, - /** A sandboxed napplet/nsite (shows the shield + "what it can access"), vs a plain web client. */ - val isSandbox: Boolean, - val onReload: () -> Unit, - val onOpenFull: () -> Unit, - /** Current Tor state, or null when this surface has no Tor toggle (Tor off / locked napplet). */ - val torOn: Boolean? = null, - val onToggleTor: () -> Unit = {}, - /** The "what it can access" sheet, for sandboxed napplets/nsites; null for a plain web client. */ - val onInfo: (() -> Unit)? = null, - /** - * Opens this app's editable permission screen (the "Connected Apps" detail) so the user can change - * trust level and per-capability grants as they browse; null when the surface has no managed identity. - */ - val onPermissions: (() -> Unit)? = null, - /** Whether the current URL/app is already saved as a favorite. */ + val state: BrowserChrome.State, val isFavorite: Boolean = false, - /** Toggles the current site/app in the favorites registry; null when not applicable. */ - val onFavorite: (() -> Unit)? = null, + val desktopSite: Boolean = false, + val textZoom: Int = BrowserChrome.DEFAULT_TEXT_ZOOM, + val onAction: (BrowserChrome.Action) -> Unit, + /** The user typed an address into "Edit address" and pressed Go. */ + val onNavigate: (String) -> Unit = {}, + val onTextZoom: (Int) -> Unit = {}, + /** The origin chip was tapped: page info (web) or the access summary (sandboxed apps). */ + val onOriginTap: () -> Unit = {}, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabLayer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabLayer.kt index 5865febe5b..7ca4b02c25 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabLayer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabLayer.kt @@ -29,6 +29,7 @@ import android.graphics.BitmapFactory import android.os.Build import android.os.SystemClock import android.view.ViewGroup +import androidx.activity.compose.BackHandler import androidx.annotation.RequiresApi import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Canvas @@ -274,11 +275,13 @@ fun EmbeddedTabLayer(barFavoriteIds: List<String>) { val chrome = EmbeddedTabHost.activeChrome val consoleBridge = activeController as? ConsoleBridge val consoleCount = consoleBridge?.consoleLogs?.size ?: 0 + val findBridge = activeController as? FindBridge if (chrome != null && bounds.width > 0f && bounds.height > 0f) { var sheetExpanded by remember(activeId) { mutableStateOf(false) } var consoleShowing by remember(activeId) { mutableStateOf(false) } var consoleExpanded by remember(activeId) { mutableStateOf(false) } + var findShowing by remember(activeId) { mutableStateOf(false) } if (sheetExpanded) { Box( @@ -304,6 +307,9 @@ fun EmbeddedTabLayer(barFavoriteIds: List<String>) { if (consoleShowing) { consoleShowing = false } else { + // One bottom panel at a time: the console replaces the find bar. + if (findShowing) findBridge?.find("") + findShowing = false consoleShowing = true consoleExpanded = true } @@ -311,6 +317,15 @@ fun EmbeddedTabLayer(barFavoriteIds: List<String>) { } else { null }, + onFind = + if (findBridge != null) { + { + consoleShowing = false + findShowing = true + } + } else { + null + }, modifier = Modifier .absoluteOffset( @@ -320,6 +335,26 @@ fun EmbeddedTabLayer(barFavoriteIds: List<String>) { ) } + // Find in page: opened via the "Find in page" row in the top pull-down sheet. + if (findShowing && findBridge != null) { + BackHandler { + findBridge.find("") + findShowing = false + } + with(density) { + EmbeddedFindBar( + bridge = findBridge, + onClose = { findShowing = false }, + modifier = + Modifier + .absoluteOffset( + (bounds.left - layerOrigin.x).toDp(), + (bounds.top - layerOrigin.y).toDp(), + ).size(bounds.width.toDp(), bounds.height.toDp()), + ) + } + } + // Bottom console panel: opened via the "Console" row in the top pull-down sheet. if (consoleShowing && consoleBridge != null) { with(density) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/FindBridge.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/FindBridge.kt new file mode 100644 index 0000000000..94fb3caf10 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/FindBridge.kt @@ -0,0 +1,43 @@ +/* + * 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.embed + +import androidx.compose.runtime.State + +/** + * A surface controller that can search its page (find in page). [EmbeddedTabLayer] draws the find bar over + * the active tab when one is open and drives the search through here. + */ +interface FindBridge { + /** The latest match count, or null before any search. */ + val findResult: State<FindResult?> + + /** Search for [query]; an empty query clears the highlights. */ + fun find(query: String) + + fun findNext(forward: Boolean) +} + +/** [active] is 0-based; [total] is 0 when nothing matched. */ +data class FindResult( + val active: Int, + val total: Int, +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/TopControlSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/TopControlSheet.kt index 8565ce9203..892cdba6d3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/TopControlSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/TopControlSheet.kt @@ -25,56 +25,82 @@ import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.Action +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.SectionKind import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols -import com.vitorpamplona.amethyst.commons.resources.Res -import com.vitorpamplona.amethyst.commons.resources.browser_reload -import com.vitorpamplona.amethyst.commons.resources.favorite_app_access_show -import com.vitorpamplona.amethyst.commons.resources.favorite_app_add -import com.vitorpamplona.amethyst.commons.resources.favorite_app_network_open -import com.vitorpamplona.amethyst.commons.resources.favorite_app_network_tor -import com.vitorpamplona.amethyst.commons.resources.favorite_app_open_window -import com.vitorpamplona.amethyst.commons.resources.favorite_app_remove -import com.vitorpamplona.amethyst.commons.resources.napplet_manage_permissions +import com.vitorpamplona.amethyst.napplethost.BrowserChromeLabels import com.vitorpamplona.amethyst.ui.stringRes +import androidx.compose.material3.Icon as Material3Icon import com.vitorpamplona.amethyst.commons.R as CommonsR /** - * A top **pull-down sheet** for a running app surface. Collapsed it's just a small grabber centered at - * the very top edge — out of the corner where a site puts its own avatar/menu. Pull it down (or tap) to - * reveal the page's controls: route over Tor, reload, "what it can access" (sandboxed apps), and open - * full screen. The page can't draw over it (the surface is z-ordered below this layer). + * A top **pull-down pill** for a running app surface. Collapsed it's just a small grabber centered at the + * very top edge — out of the corner where a site puts its own avatar/menu. Pulled down (or tapped) it shows a + * Chrome-PWA-style app menu: the page title with its origin and connection badge (tap for page info), the + * icon row (back · forward · reload/stop · star · share), then the menu rows and the Privacy and Developer + * groups. The page can't draw over it (the surface is z-ordered below this layer). * - * @param onConsole When non-null, a "Console (N)" row is shown so the user can toggle the log - * panel from the pull-down sheet. [consoleCount] is the number of messages already captured. + * The layout comes from [BrowserChrome] and the icons/labels from [BrowserChromeLabels] — both shared with + * the full-screen browser's native sheet — so an action looks and sits the same in either. + * + * @param onConsole When non-null, CONSOLE toggles the log panel through it ([consoleCount] messages so far). + * @param onFind When non-null, FIND_IN_PAGE opens the find bar through it. */ @Composable fun TopControlSheet( @@ -85,7 +111,19 @@ fun TopControlSheet( consoleCount: Int = 0, consoleShowing: Boolean = false, onConsole: (() -> Unit)? = null, + onFind: (() -> Unit)? = null, ) { + val state = remember(chrome.state, onConsole) { if (onConsole == null) chrome.state.copy(hasConsole = false) else chrome.state } + var editingAddress by remember(expanded) { mutableStateOf(false) } + + fun act(action: Action) { + when (action) { + Action.CONSOLE -> onConsole?.invoke() + Action.FIND_IN_PAGE -> onFind?.invoke() + else -> chrome.onAction(action) + } + } + Column( modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, @@ -108,75 +146,68 @@ fun TopControlSheet( shape = RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp), ) { Column(Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 6.dp)) { - Row(Modifier.padding(horizontal = 8.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically) { - Icon( - if (chrome.isSandbox) MaterialSymbols.Security else MaterialSymbols.Public, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = if (chrome.isSandbox) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(Modifier.width(10.dp)) - Text( - chrome.title, - style = MaterialTheme.typography.titleMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + SheetHeader( + chrome = chrome, + state = state, + editingAddress = editingAddress, + onOriginTap = { + onExpandedChange(false) + chrome.onOriginTap() + }, + onCopy = { chrome.onAction(Action.COPY_LINK) }, + onNavigate = { text -> + editingAddress = false + onExpandedChange(false) + chrome.onNavigate(text) + }, + ) + IconActionRow(state, chrome.isFavorite) { action -> + onExpandedChange(false) + act(action) } HorizontalDivider() - chrome.torOn?.let { torOn -> - SheetSwitchItem( - symbol = MaterialSymbols.Lock, - label = stringRes(if (torOn) Res.string.favorite_app_network_tor else Res.string.favorite_app_network_open), - checked = torOn, - onToggle = { chrome.onToggleTor() }, - ) - } - SheetItem(MaterialSymbols.Refresh, stringRes(Res.string.browser_reload)) { - onExpandedChange(false) - chrome.onReload() - } - chrome.onInfo?.let { info -> - SheetItem(MaterialSymbols.Info, stringRes(Res.string.favorite_app_access_show)) { - onExpandedChange(false) - info() + val maxHeight = (LocalConfiguration.current.screenHeightDp * 0.55f).dp + Column(Modifier.heightIn(max = maxHeight).verticalScroll(rememberScrollState())) { + BrowserChrome.sections(state).forEachIndexed { index, section -> + if (index > 0) HorizontalDivider(Modifier.padding(vertical = 2.dp)) + sectionTitle(section.kind)?.let { + Text( + it.uppercase(), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 8.dp, top = 10.dp, bottom = 2.dp), + ) + } + section.actions.forEach { action -> + when { + action == Action.TEXT_SIZE -> TextSizeRow(chrome.textZoom, chrome.onTextZoom) + BrowserChromeLabels.isToggle(action) -> + SheetSwitchItem( + action = action, + label = rowLabel(action, state, chrome.isFavorite, consoleCount), + checked = + when (action) { + Action.TOR -> state.torOn == true + Action.DESKTOP_SITE -> chrome.desktopSite + else -> consoleShowing + }, + ) { + onExpandedChange(false) + act(action) + } + else -> + SheetItem(action, rowLabel(action, state, chrome.isFavorite, consoleCount)) { + if (action == Action.EDIT_ADDRESS) { + editingAddress = true + } else { + onExpandedChange(false) + act(action) + } + } + } + } } } - chrome.onPermissions?.let { openPermissions -> - SheetItem(MaterialSymbols.Tune, stringRes(Res.string.napplet_manage_permissions)) { - onExpandedChange(false) - openPermissions() - } - } - SheetItem(MaterialSymbols.OpenInFull, stringRes(Res.string.favorite_app_open_window)) { - onExpandedChange(false) - chrome.onOpenFull() - } - chrome.onFavorite?.let { toggleFavorite -> - SheetItem( - if (chrome.isFavorite) MaterialSymbols.Star else MaterialSymbols.StarBorder, - stringRes(if (chrome.isFavorite) Res.string.favorite_app_remove else Res.string.favorite_app_add), - ) { - onExpandedChange(false) - toggleFavorite() - } - } - onConsole?.let { showConsole -> - SheetSwitchItem( - symbol = MaterialSymbols.Code, - label = - if (consoleCount > 0) { - stringRes(CommonsR.string.browser_console_title, consoleCount) - } else { - stringRes(CommonsR.string.browser_console_title_short) - }, - checked = consoleShowing, - onToggle = { - onExpandedChange(false) - showConsole() - }, - ) - } } } } @@ -214,9 +245,152 @@ fun TopControlSheet( } } +@Composable +private fun sectionTitle(kind: SectionKind): String? = + when (kind) { + SectionKind.PAGE -> null + SectionKind.PRIVACY -> stringRes(CommonsR.string.browser_section_privacy) + SectionKind.DEVELOPER -> stringRes(CommonsR.string.browser_section_developer) + } + +@Composable +private fun rowLabel( + action: Action, + state: BrowserChrome.State, + isFavorite: Boolean, + consoleCount: Int, +): String = + when { + action == Action.BACK_TO_APP -> stringRes(CommonsR.string.browser_action_back_to_app, BrowserChrome.displayHost(state.startUrl)) + action == Action.CONSOLE && consoleCount > 0 -> stringRes(CommonsR.string.browser_console_title, consoleCount) + else -> stringRes(BrowserChromeLabels.labelFor(action, isFavorite = isFavorite, torOn = state.torOn == true)) + } + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun SheetHeader( + chrome: EmbeddedTabChrome, + state: BrowserChrome.State, + editingAddress: Boolean, + onOriginTap: () -> Unit, + onCopy: () -> Unit, + onNavigate: (String) -> Unit, +) { + val security = BrowserChrome.security(state) + Row(Modifier.padding(horizontal = 8.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically) { + SymbolOrDrawable( + BrowserChromeLabels.securitySymbol(security), + BrowserChromeLabels.securityDrawable(security), + tint = if (state.isSandbox) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, + size = 20, + ) + Spacer(Modifier.width(12.dp)) + if (editingAddress) { + AddressField(state.url, onNavigate, Modifier.weight(1f)) + } else { + Column( + Modifier + .weight(1f) + .clip(RoundedCornerShape(8.dp)) + .combinedClickable(onClick = onOriginTap, onLongClick = if (state.isSandbox) null else onCopy), + ) { + Text(chrome.title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis) + val securityLabel = stringRes(BrowserChromeLabels.securityLabel(security)) + Text( + if (state.isSandbox) securityLabel else BrowserChrome.displayHost(state.url) + " · " + securityLabel, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} + +/** The rarely used editable address, swapped in for the origin chip by "Edit address". */ +@Composable +private fun AddressField( + url: String, + onNavigate: (String) -> Unit, + modifier: Modifier = Modifier, +) { + var field by remember { mutableStateOf(TextFieldValue(url, TextRange(0, url.length))) } + val focus = remember { FocusRequester() } + LaunchedEffect(Unit) { focus.requestFocus() } + TextField( + value = field, + onValueChange = { field = it }, + modifier = modifier.focusRequester(focus), + singleLine = true, + placeholder = { Text(stringRes(CommonsR.string.browser_address_hint)) }, + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.None, + autoCorrectEnabled = false, + keyboardType = KeyboardType.Uri, + imeAction = ImeAction.Go, + ), + keyboardActions = + KeyboardActions(onGo = { + field.text + .trim() + .takeIf { it.isNotEmpty() } + ?.let(onNavigate) + }), + colors = + TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + ) +} + +@Composable +private fun IconActionRow( + state: BrowserChrome.State, + isFavorite: Boolean, + onAction: (Action) -> Unit, +) { + Row(Modifier.fillMaxWidth().padding(vertical = 2.dp), horizontalArrangement = Arrangement.SpaceEvenly) { + BrowserChrome.iconRow(state).forEach { action -> + val enabled = BrowserChrome.isEnabled(state, action) + val favorite = action == Action.FAVORITE && isFavorite + IconButton(onClick = { onAction(action) }, enabled = enabled) { + Icon( + BrowserChromeLabels.symbolFor(action)!!, + contentDescription = stringRes(BrowserChromeLabels.labelFor(action, isFavorite = isFavorite)), + modifier = Modifier.size(24.dp), + tint = + when { + !enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.35f) + favorite -> MaterialTheme.colorScheme.primary + else -> MaterialTheme.colorScheme.onSurface + }, + filled = favorite, + ) + } + } + } +} + +@Composable +private fun SymbolOrDrawable( + symbol: MaterialSymbol?, + drawable: Int?, + tint: Color, + size: Int, +) { + if (drawable != null) { + Material3Icon(painterResource(drawable), contentDescription = null, modifier = Modifier.size(size.dp), tint = tint) + } else if (symbol != null) { + Icon(symbol, contentDescription = null, modifier = Modifier.size(size.dp), tint = tint) + } +} + @Composable private fun SheetItem( - symbol: MaterialSymbol, + action: Action, label: String, onClick: () -> Unit, ) { @@ -228,7 +402,7 @@ private fun SheetItem( .padding(horizontal = 8.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, ) { - Icon(symbol, contentDescription = null, modifier = Modifier.size(22.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant) + SymbolOrDrawable(BrowserChromeLabels.symbolFor(action), BrowserChromeLabels.drawableFor(action), MaterialTheme.colorScheme.onSurfaceVariant, 22) Spacer(Modifier.width(14.dp)) Text(label, style = MaterialTheme.typography.bodyLarge) } @@ -236,7 +410,7 @@ private fun SheetItem( @Composable private fun SheetSwitchItem( - symbol: MaterialSymbol, + action: Action, label: String, checked: Boolean, onToggle: () -> Unit, @@ -251,9 +425,35 @@ private fun SheetSwitchItem( .padding(horizontal = 8.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, ) { - Icon(symbol, contentDescription = null, modifier = Modifier.size(22.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant) + SymbolOrDrawable(BrowserChromeLabels.symbolFor(action), BrowserChromeLabels.drawableFor(action), MaterialTheme.colorScheme.onSurfaceVariant, 22) Spacer(Modifier.width(14.dp)) Text(label, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f)) Switch(checked = checked, onCheckedChange = { onToggle() }) } } + +@Composable +private fun TextSizeRow( + textZoom: Int, + onTextZoom: (Int) -> Unit, +) { + Row( + Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(MaterialSymbols.FormatSize, contentDescription = null, modifier = Modifier.size(22.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant) + Spacer(Modifier.width(14.dp)) + Text(stringRes(CommonsR.string.browser_action_text_size), style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f)) + IconButton(onClick = { onTextZoom(BrowserChrome.stepTextZoom(textZoom, larger = false)) }) { + Icon(MaterialSymbols.Remove, contentDescription = stringRes(CommonsR.string.browser_action_text_smaller)) + } + Text( + stringRes(CommonsR.string.browser_action_text_size_value, textZoom), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.widthIn(min = 44.dp), + ) + IconButton(onClick = { onTextZoom(BrowserChrome.stepTextZoom(textZoom, larger = true)) }) { + Icon(MaterialSymbols.Add, contentDescription = stringRes(CommonsR.string.browser_action_text_larger)) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt index 1067fce67e..6a4a87eb83 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt @@ -54,6 +54,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.navigation.favoriteIds @@ -174,20 +175,37 @@ private fun EmbeddedNostrAppTab( remember(title, coordinate, isFavorite, controller) { EmbeddedTabChrome( title = title.ifBlank { coordinate }, - isSandbox = true, - onReload = { controller.reload() }, - onOpenFull = { FavoriteAppLauncher.launch(context, FavoriteApp.NostrApp(coordinate, title, System.currentTimeMillis()), appStillLoadingStr) }, - onInfo = { showAccess = true }, - onPermissions = { nav.nav(Route.ConnectedAppDetail(permissionCoordinate)) }, + state = + BrowserChrome.State( + surface = BrowserChrome.Surface.NAPPLET, + presentation = BrowserChrome.Presentation.EMBEDDED, + url = "", + startUrl = "", + hasAccessInfo = true, + // The embedded nsite/napplet host has no find or text-size plumbing (yet). + hasFind = false, + hasTextSize = false, + ), isFavorite = isFavorite, - onFavorite = { - val favId = "nostr:$coordinate" - if (FavoriteAppsRegistry.isFavorite(favId)) { - FavoriteAppsRegistry.remove(favId) - } else { - FavoriteAppsRegistry.add(FavoriteApp.NostrApp(coordinate, title, System.currentTimeMillis())) + onAction = { action -> + when (action) { + BrowserChrome.Action.RELOAD -> controller.reload() + BrowserChrome.Action.OPEN_FULL_SCREEN -> + FavoriteAppLauncher.launch(context, FavoriteApp.NostrApp(coordinate, title, System.currentTimeMillis()), appStillLoadingStr) + BrowserChrome.Action.ACCESS_INFO -> showAccess = true + BrowserChrome.Action.SITE_SETTINGS -> nav.nav(Route.ConnectedAppDetail(permissionCoordinate)) + BrowserChrome.Action.FAVORITE -> { + val favId = "nostr:$coordinate" + if (FavoriteAppsRegistry.isFavorite(favId)) { + FavoriteAppsRegistry.remove(favId) + } else { + FavoriteAppsRegistry.add(FavoriteApp.NostrApp(coordinate, title, System.currentTimeMillis())) + } + } + else -> Unit } }, + onOriginTap = { showAccess = true }, ) } // Publish the top-sheet controls to the tab layer (drawn over the z-below surface). In a SideEffect diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt index ed86de0cb2..fa9308a6be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt @@ -37,6 +37,8 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -62,6 +64,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission import com.vitorpamplona.amethyst.commons.browser.OmniboxInput import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46ClientInfo import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46PermissionAuthorizer @@ -114,6 +117,7 @@ import com.vitorpamplona.amethyst.favorites.BrowserIconRegistry import com.vitorpamplona.amethyst.favorites.rememberManifestIconModel import com.vitorpamplona.amethyst.favorites.rememberWebAppIconModel import com.vitorpamplona.amethyst.napplet.NappletBrokerService +import com.vitorpamplona.amethyst.napplet.WebSitePermissionRegistry import com.vitorpamplona.amethyst.napplet.counterpartyLabel import com.vitorpamplona.amethyst.napplet.descriptionRes import com.vitorpamplona.amethyst.napplet.labelRes @@ -321,6 +325,11 @@ fun ConnectedAppDetailScreen( } } + // Camera / microphone / location answers for a website (`browser:<origin>`), editable here. + if (coordinate.startsWith(BROWSER_PREFIX)) { + SitePermissionsSection(coordinate.removePrefix(BROWSER_PREFIX)) + } + // Recent activity (NIP-46 clients only) if (nip46Client != null && nip46Activity.isNotEmpty()) { SectionHeader(stringRes(Res.string.nip46_signer_activity_title)) @@ -341,6 +350,11 @@ fun ConnectedAppDetailScreen( signerLedger.revokeAll(coordinate) } capabilityLedger.revokeAll(identity) + // A forgotten website also loses its camera / microphone / location answers. + if (coordinate.startsWith(BROWSER_PREFIX)) { + val origin = coordinate.removePrefix(BROWSER_PREFIX) + BrowserSitePermission.entries.forEach { WebSitePermissionRegistry.set(origin, it, BrowserSitePermission.Decision.ASK) } + } // Forgetting an app has to stop it signing *now*. The two ledgers above only // drop persisted + capability grants; the broker separately holds the signer's // in-memory "allow for this session" grants, which would otherwise keep the @@ -534,6 +548,69 @@ private fun AppIdentityHeader(state: ConnectedAppDetailState) { } } +private const val BROWSER_PREFIX = "browser:" + +/** + * The site's camera / microphone / location answers ([WebSitePermissionRegistry]), each switchable between + * Ask, Allow and Block — Chrome's per-site permission list. + */ +@Composable +private fun SitePermissionsSection(origin: String) { + val all by WebSitePermissionRegistry.decisions.collectAsStateWithLifecycle() + val decisions = all[origin].orEmpty() + SectionHeader(stringRes(CommonsR.string.browser_permission_section)) + Surface( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = MaterialTheme.shapes.medium, + modifier = Modifier.fillMaxWidth(), + ) { + Column(modifier = Modifier.padding(4.dp)) { + BrowserSitePermission.entries.forEachIndexed { index, permission -> + if (index > 0) HorizontalDivider(modifier = Modifier.padding(horizontal = 12.dp)) + val decision = decisions[permission] ?: BrowserSitePermission.Decision.ASK + var menuOpen by remember { mutableStateOf(false) } + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + stringRes( + when (permission) { + BrowserSitePermission.CAMERA -> CommonsR.string.browser_permission_camera + BrowserSitePermission.MICROPHONE -> CommonsR.string.browser_permission_microphone + BrowserSitePermission.LOCATION -> CommonsR.string.browser_permission_location + }, + ), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.weight(1f), + ) + Box { + TextButton(onClick = { menuOpen = true }) { Text(stringRes(decision.labelRes())) } + DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) { + BrowserSitePermission.Decision.entries.forEach { option -> + DropdownMenuItem( + text = { Text(stringRes(option.labelRes())) }, + onClick = { + menuOpen = false + WebSitePermissionRegistry.set(origin, permission, option) + }, + ) + } + } + } + } + } + } + } +} + +private fun BrowserSitePermission.Decision.labelRes(): Int = + when (this) { + BrowserSitePermission.Decision.ASK -> CommonsR.string.browser_permission_ask + BrowserSitePermission.Decision.ALLOW -> CommonsR.string.browser_permission_allowed + BrowserSitePermission.Decision.BLOCK -> CommonsR.string.browser_permission_blocked + } + @Composable private fun SectionHeader(text: String) { Text( From 40d06f4152b3203b85543fd3c05c50f546f9c889 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 16:15:45 +0000 Subject: [PATCH 08/60] docs(plans): record browser PWA-parity implementation Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TEkj7Eo2xbidVHAoF8GZKQ --- .../plans/2026-09-26-browser-pwa-parity.md | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/amethyst/plans/2026-09-26-browser-pwa-parity.md b/amethyst/plans/2026-09-26-browser-pwa-parity.md index 0321a63f50..115e1678a3 100644 --- a/amethyst/plans/2026-09-26-browser-pwa-parity.md +++ b/amethyst/plans/2026-09-26-browser-pwa-parity.md @@ -1,6 +1,6 @@ # Browser surfaces → PWA parity review -Status: **review / proposal**, decisions recorded in §6. Scope: every surface that renders a +Status: **implemented** (phases 0–5, see §7), decisions recorded in §6. Not yet verified on a device. Scope: every surface that renders a plain web client — the **embedded** bottom-bar tab and the **external** full-screen browser — plus their shared top pull-down "pill" and bottom console "pill". The nsite/napplet hosts reuse the same chrome and are covered where the change is shared. @@ -287,3 +287,71 @@ nsite/napplet hosts (sandbox profile permitting). 4. **Add to Home screen:** the shortcut opens the page full screen in **Amethyst's browser** (not the system browser), so the signer is present. It launches through a main-process trampoline. + +## 7. Implementation (2026-09-26) + +What shipped, where it lives, and what was deliberately left out. + +**Shared layout.** `commons/…/browser/BrowserChrome.kt` decides which actions the top pill shows, and in +what order, for every surface (web / nsite / napplet × embedded / full screen), plus the security badge, +the scope check, text-zoom steps, the desktop user agent and the theme-colour parser. It is covered by +`BrowserChromeTest`. `nappletHost/…/BrowserChromeLabels.kt` maps each action to one Material Symbol and +one label (shared strings in `commons` Android resources). Both renderers draw from these two: + +- `TopControlSheet` (Compose, embedded tabs). +- `NappletControlSheet` (plain Views, full screen). It loads the same Material Symbols font from the + `commonsUI` assets, so the icons match. Three glyphs were added to the subset font: `FormatSize`, + `DesktopWindows` and `AddToHomeScreen`. + +**Top pill.** Header: security icon, page title, and `host · connection`. Tapping it opens page info; +long-pressing copies the link. The full-screen header also has ✕. Below it: +- The icon row: back · forward · reload/stop · star (filled when pinned) · share. +- Menu rows: back to app, copy link, edit address, find in page, text size, desktop site, add to Home + screen, open in another browser, open full screen. +- Privacy: Tor, what it can access, site settings. +- Developer: console. + +**Bottom pill.** Console as before; find in page docks in the same slot, one panel at a time. + +**Web platform** (`BrowserWebTools`, `BrowserDownloads`, `BrowserPopups`, `BrowserExtrasScript`, +`BrowserJsDialogs`; used by both `NappletBrowserActivity` and `NappletBrowserService`): +- JS dialogs: native in full screen; relayed to Compose for the embed. +- `_blank` / `window.open` open as a new full-screen window, with `opener` kept through a parked popup + WebView. +- `intent:` URIs are parsed, hardened, and fall back to `browser_fallback_url`. +- Downloads follow the page's route (Tor through SOCKS, remote DNS), carry the page's cookies, and land + in Downloads. `blob:` and `data:` downloads come through the page script. +- HTML fullscreen: the whole window in full screen; inside the surface for the embed. +- `navigator.share` polyfill (text/url). +- Renderer-crash recovery in all four WebView owners. + +**OS integration.** +- `theme-color` tints the full-screen window's system-bar areas. +- `setTaskDescription` sets the title, favicon and colour in Recents. +- Add to Home screen (`WebShortcuts` + `WebShortcutActivity`, main process, waits for Tor). +- Dynamic launcher shortcuts for the first four web favorites. + +**Permissions and page info.** +- Camera, microphone and location go through a per-origin prompt. Answers are kept in + `WebSitePermissionRegistry` (main process, same on Tor and open web), then Android's runtime permission + is requested. +- The Connected Apps detail screen lists and resets those answers. +- Page info shows the connection, Tor and certificate, with Clear site data (this profile only). + +**Left out, and why.** +- **Pull-to-refresh:** WebView exposes no overscroll signal, so on pages that scroll an inner element + (most SPAs) `scrollY == 0` is always true. A pull there would reload mid-scroll. +- **Long-press menu in the embedded tab:** the SurfaceControlViewHost surface doesn't deliver long-press + context menus. It is available in the full-screen browser. +- **Theme colour in the embedded tab:** the tab owns no system bars. The script's message is ignored + there. +- **Find / text size for embedded nsites and napplets:** that host has no IPC for them yet. The rows are + hidden via `BrowserChrome.State.hasFind` / `hasTextSize`. The full-screen nsite/napplet host has both. +- **Notifications / Push, file sharing through Web Share:** unchanged, see §4.5. + +**Needs on-device verification.** +- Popup `opener` handoff across activities. +- Renderer-crash recovery. Trigger it with `chrome://crash` in a debug build, or by killing the renderer. +- Downloads over Tor. +- The pinned-shortcut cold start with Tor enabled. + From 7267d6c359b61c2b2e5336025b8b1725ee7494b9 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 17:33:13 +0000 Subject: [PATCH 09/60] design(browser): UI review and Compose prototypes of the redesigned pill Review of the shipped browser chrome (menu hierarchy, mute states, bare inputs, stock dialogs, Views/Compose parity) and a redesign: navigation capsule, page-action tiles, privacy card with plain-language consequences, a stateful collapsed handle, a first-class address editor with suggestions and paste-and-go, a floating find pill, a filterable console sheet, a three-choice permission prompt, origin-badged page dialogs, and a page-info sheet with in-place permission choices and a confirmed clear. Stateless composables in commonsUI (browser/ui/pill) with previews; BrowserPillRenderTest renders every screen offscreen in both themes to commonsUI/build/browser-pill/. Adds Cookie, NorthWest and ContentPasteGo glyphs to the Material Symbols subset. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TEkj7Eo2xbidVHAoF8GZKQ --- .../plans/2026-09-26-browser-ui-review.md | 236 ++++++++ .../font/material_symbols_outlined.ttf | Bin 498440 -> 504348 bytes .../composeResources/values/strings.xml | 100 ++++ .../commons/browser/ui/pill/AddressEditor.kt | 219 ++++++++ .../commons/browser/ui/pill/BrowserPill.kt | 503 ++++++++++++++++++ .../browser/ui/pill/BrowserPillModel.kt | 90 ++++ .../browser/ui/pill/BrowserPillPreviews.kt | 240 +++++++++ .../commons/browser/ui/pill/ConsoleSheet.kt | 231 ++++++++ .../commons/browser/ui/pill/FindInPagePill.kt | 135 +++++ .../commons/browser/ui/pill/PageSheets.kt | 476 +++++++++++++++++ .../commons/browser/ui/pill/PillComponents.kt | 348 ++++++++++++ .../commons/browser/ui/pill/PillIcons.kt | 215 ++++++++ .../commons/icons/symbols/MaterialSymbols.kt | 3 + .../browser/ui/pill/BrowserPillRenderTest.kt | 96 ++++ 14 files changed, 2892 insertions(+) create mode 100644 amethyst/plans/2026-09-26-browser-ui-review.md create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/AddressEditor.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPill.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillModel.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillPreviews.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/ConsoleSheet.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/FindInPagePill.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/PageSheets.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/PillComponents.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/PillIcons.kt create mode 100644 commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillRenderTest.kt diff --git a/amethyst/plans/2026-09-26-browser-ui-review.md b/amethyst/plans/2026-09-26-browser-ui-review.md new file mode 100644 index 0000000000..58d0284e59 --- /dev/null +++ b/amethyst/plans/2026-09-26-browser-ui-review.md @@ -0,0 +1,236 @@ +# Browser surfaces: UI review and redesign + +Status: **review + Compose prototypes**. The prototypes live in +`commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/`. They are rendered +offscreen by `BrowserPillRenderTest` (commonsUI jvmTest), which writes PNGs to +`commonsUI/build/browser-pill/`. None of it is wired into the app yet (see §5). + +Follows `2026-09-26-browser-pwa-parity.md`. That plan settled *what* the browser does. This one is about +how it looks and feels. + +## 1. What's wrong with what shipped + +Reviewed against the code as merged: `TopControlSheet`, `NappletControlSheet`, `BottomConsoleSheet`, +`NappletConsolePanel`, `BrowserFindBar`, `EmbeddedFindBar`, `EmbeddedPageDialogs`, `BrowserJsDialogs` +and the full-screen permission / page-info `AlertDialog`s. + +**Top pill** +1. **It's a settings list, not a menu.** Up to 15 rows of equal weight in one scrolling column. Share, + copy and find (used daily) look exactly like desktop site and open-in-another-browser (used rarely). + Chrome solves this with an icon strip plus a short list; we copied the list and made it longer. +2. **No visual hierarchy.** Section labels are 12sp caps you don't notice. Every row is the same 44dp + icon + label. Nothing groups. +3. **Security state is a word, not a signal.** "Not secure" and "Onion-routed over Tor" are the same + grey 13sp text. Plain HTTP isn't warned about, and Tor, the thing Amethyst does that Chrome doesn't, + isn't celebrated. +4. **Three rows are just switches** (Tor, desktop site, console). Each is a full-width row whose only + content is the switch. They say what they are, not what they mean ("Loads over Tor" vs "the site + can't see your IP"). +5. **The text-size stepper** is squeezed into a list row: two 24dp buttons and a percentage, with no + preview and no way back to 100%. +6. **The collapsed grabber is mute.** It never shows loading, an insecure page, or console errors, so + there's nothing to invite the pull. + +**Inputs** + +7. **Edit address is a bare text field** dropped into the header. It has no clear button, no go button, + no suggestions (the launcher's omnibox has them), and no paste-and-go. +8. **Find in page** is a full-width sheet with a default `TextField`. The count is loose text, and "no + matches" has no state of its own. The native version draws a different bar. +9. **The JS prompt field** has no label. "Block dialogs from this page" is a third button squeezed in + beside Cancel/OK. Chrome uses a checkbox, because it's an option, not an answer. + +**Dialogs and sheets** + +10. **The permission prompt** is a stock `AlertDialog` with bullet text and only Allow / Block. There's + no "just this time", no icon for what's being asked, and nothing about the connection. +11. **Page info** is a paragraph in an `AlertDialog` with three crowded buttons. The site's permissions + aren't there, and Clear site data runs immediately, with no confirmation. +12. **The console** is a flat list with no filters (errors are buried in logs), no copy, and a count + that only shows inside the menu. + +**Parity** + +13. **Two renderers, two looks.** The full-screen pill is plain Views: framework `Switch`, framework + ripple, hand-rolled type sizes. Sharing the layout spec stopped the two drifting in content, but + they still don't look alike. `:nappletHost` already depends on `:commonsUI` (Compose), so the + full-screen window can host the same Compose pill in a `ComposeView`. + +## 2. Principles + +- **One tap for daily things, two for rare ones.** Navigation, star and share sit in a capsule. + Page actions are a grid of tiles. Privacy and developer settings are grouped cards below. +- **State is visible, even collapsed.** The security colour (error for HTTP, Tor accent for onion), + loading progress and console errors show on the grabber itself. +- **Inputs are first-class.** Every text input has: + - a real container and a leading icon that says what it is; + - a placeholder or label; + - clear and submit buttons; + - the right keyboard and IME action; + - an empty or error state. + + Choices are segmented buttons or switches that state their consequence, never a bare "on". +- **Explain consequences in plain words.** Say "The site can't see your IP address", not "Loads over + Tor". Say "Allow while visiting / Only this time / Don't allow", not "Allow / Block". +- **One implementation.** Stateless composables in `commonsUI`, driven by `BrowserChrome` (already + shared). The embedded tab, the full-screen window (through `ComposeView`) and a future desktop browser + all draw the same pixels. + +## 3. The redesign + +### 3.1 Collapsed handle + +A 48×24dp capsule at the top centre holding a 32dp bar. +- A 2dp progress line runs under the bar while the page loads. +- The bar takes the error colour on HTTP and the Tor accent when onion-routed. +- A 6dp error dot appears at the right edge when the console has errors. +- It keeps its current gestures: pull or tap to open. + +### 3.2 Expanded pill + +``` +╭────────────────────────────────────────────╮ +│ [P] Primal ( ✕ ) │ monogram tile · title · close (full screen) +│ ╭────────────────────────────────────────╮ │ +│ │ 🧅 primal.net ✎ │ │ origin field: tap edits the address +│ ╰────────────────────────────────────────╯ │ +│ ⓘ You left primal.net [ Back to app ] │ only when out of scope +│ ╭────────────────────────────────────────╮ │ +│ │ ← → ↻ ★ ⇪ │ │ navigation capsule +│ ╰────────────────────────────────────────╯ │ +│ ╭────╮ ╭────╮ ╭────╮ ╭────╮ │ +│ │ ⧉ │ │ 🔍 │ │ Aa │ │ ⌂+ │ │ page actions (tiles, 4 per row) +│ │Copy│ │Find│ │Text│ │Home│ │ +│ ╰────╯ ╰────╯ ╰────╯ ╰────╯ │ +│ ╭────╮ ╭────╮ ╭────╮ │ toggle tiles fill when on (Desktop) +│ │ 🖥 │ │ ⬈ │ │ ⛶ │ │ +│ ╰────╯ ╰────╯ ╰────╯ │ +│ A ━━━━━━●━━━━━━━ A 115% Reset │ appears when "Text" is on +│ ╭ Privacy ───────────────────────────────╮ │ +│ │ (🧅) Onion routing [ ● ] │ │ +│ │ The site can't see your IP │ │ +│ │ (⚙) Site settings › │ │ +│ │ Camera allowed · Location blocked │ │ +│ ╰────────────────────────────────────────╯ │ +│ >_ Console (3 errors) [ ] │ +╰────────────────────────────────────────────╯ +``` + +- **Origin field.** It looks like an input on purpose (the address is one tap away without an address + bar taking space): + - a pill-shaped container on `surfaceContainerHighest`; + - the security icon in its colour; + - the host in `titleSmall`; + - a trailing pencil. + + Tapping it opens the address editor (§3.3). Long-pressing copies the link. +- **Navigation capsule.** Five 48dp icon buttons on `surfaceContainerHigh`, `CircleShape`. Back and + forward dim when they can't be used. The star fills in `primary` when the page is pinned. Reload + becomes Stop, with a progress ring, while loading. +- **Tiles.** 72dp-tall rounded (16dp) cards on `surfaceContainer`, each with an icon and a two-line + `labelMedium` label. Toggle tiles (desktop site, text size open) switch to `secondaryContainer` with + a check badge. +- **Privacy card.** Grouped rows on one rounded container: + - each row has a leading icon in a 36dp tinted circle, a title, and a supporting line that states the + consequence; + - a trailing switch, or a chevron for navigation; + - site settings summarise camera / mic / location decisions inline. +- **Developer row.** The console with an error badge (`errorContainer` pill) and a switch. It's the + only developer item, so it gets no heading. +- **Sandboxed apps** use the same frame: + - the origin field reads "Sandboxed app" with a shield and doesn't edit; + - the capsule is reload + star; + - the tiles are find and text size; + - "What it can access" joins the privacy card. + +### 3.3 Address editor (first-class input) + +Replaces the origin field inside the pill: +- A 56dp pill field with the security/search icon leading, the URL selected, and trailing clear (✕) + and Go (a filled `primary` circle with an arrow). The keyboard is URI type with IME Go. +- Below it: + - a **Paste and go** chip when the clipboard holds a URL; + - then up to five suggestions (favorites first, then history, from `OmniboxSuggestions`), each with a + monogram, the title and the URL; + - each suggestion has a trailing ↖ that fills its URL into the field without going. +- Back or tapping outside collapses it to the origin field. + +### 3.4 Bottom: find in page and console + +- **Find pill.** A floating 56dp capsule, 12dp above the bottom edge, with shadow. It holds: + - a search icon; + - the field with placeholder "Find in page"; + - a match chip ("3 / 12", tonal; "No matches" in `errorContainer`); + - up/down, a divider, then close. + + The IME Search action jumps to the next match. +- **Console sheet.** A drag handle, then a title with filter chips "All 42 · Errors 3 · Warnings 5", + then Copy and Clear. Log rows have: + - a 3dp level-coloured stripe; + - the message in monospace; + - `file:line` muted on the right; + - long-press to copy. + + Errors show first when the Errors chip is on. + +### 3.5 Permission prompt + +A card sheet: +- the origin field (read-only) on top; +- 48dp tinted icon circles for each thing asked (camera / mic / location); +- a title in plain words ("Use your camera and microphone?") and one body line; +- when the site is on Tor and asks for camera or mic, a muted note: "Calls can reveal your IP address + even over Tor". + +Three stacked full-width buttons: **Allow while visiting** (filled, remembered), **Only this time** +(tonal, not remembered), **Don't allow** (text, remembered). + +### 3.6 JS dialogs + +A card with: +- the origin field as its header (so a page can't spoof Amethyst UI); +- the message in `bodyLarge`, scrolling past 40% of the screen; +- for `prompt`, an `OutlinedTextField` with a label, a clear button, autofocus, and IME Done that + submits; +- a **"Don't let this page show more dialogs"** checkbox (second dialog onward); +- right-aligned Cancel / OK. "Leave site?" uses a destructive-tinted Leave. + +### 3.7 Page info + +A sheet with: +- **Header:** monogram, host, and the full origin. +- **Connection:** rows with icons: encryption state (a warning tone for HTTP), then the route (Tor or + open web, with the same consequence line as the pill), then the certificate: issued to, issued by, + and expiry. +- **Permissions:** one row per camera / mic / location with a segmented button **Ask · Allow · + Block**, editable in place. +- **Cookies and site data:** an outlined destructive button, "Clear site data". The first tap turns it + into an inline confirmation ("Sign out of this site and delete its data? · Cancel · Clear") instead + of acting at once. + +## 4. Tokens + +| Use | Token | +|---|---| +| Pill surface | `surface`, 0 tonal elevation, 6dp shadow, 24dp bottom corners | +| Inputs, origin field | `surfaceContainerHighest`, `CircleShape` | +| Navigation capsule | `surfaceContainerHigh`, `CircleShape` | +| Tiles, grouped cards | `surfaceContainer`, 16dp | +| Toggle "on" | `secondaryContainer` / `onSecondaryContainer` | +| Insecure | `error` / `errorContainer` | +| Tor accent | `tertiary` (no new colour, readable in both themes) | +| Spacing | 4dp grid, 16dp sheet padding, 8dp between tiles | +| Type | title `titleMedium`, origin `titleSmall`, tiles `labelMedium`, supporting `bodySmall` | + +## 5. Next steps + +1. Wire the prototypes in: `TopControlSheet` → `BrowserPill`, `EmbeddedFindBar` → `FindInPagePill`, + `BottomConsoleSheet` → `ConsoleSheet`, and the embedded dialogs → `PermissionPromptCard` / + `PageDialogCard` / `PageInfoSheet`. +2. Host the same composables in the full-screen window through `ComposeView`, and delete + `NappletControlSheet`, `BrowserFindBar`, `NappletConsolePanel` and `BrowserJsDialogs`. +3. Move the pill's labels from the `commons` Android resources to the Compose catalogue (the + prototypes already use `Res.string.browser_pill_*`). +4. Add "Only this time" to the permission flow (the registry already treats "no answer" as + not-remembered). +5. Confirm before clearing site data in both surfaces. diff --git a/commonsUI/src/commonMain/composeResources/font/material_symbols_outlined.ttf b/commonsUI/src/commonMain/composeResources/font/material_symbols_outlined.ttf index 3af0f83bd12753dca85367876b4f9743284cd835..05533f6459d8955444431ba52cdce4a0eacc3b6d 100644 GIT binary patch delta 9235 zcmbt(3w%_?_5Ycfd+)wCyUD(?Np?4zCn1o8JP3~vK;Ewe5VfFScnC#a0!5(O-6W_~ zttdfhQ7Qe4;)D8VpgxM?tChdSN)ah4LZT27vV_fMv%A^n@7xUup|wB#{Qnc?&fJ+Z zGiT16Ip6PHw*T4k%mqs>5CG8Pg9FXvx%0;Fz4QO90qEHXK=x0$W!xO{0%QS%Oi$!m z9H9v)`*F>tn)RS;dmSJw#rNtZt83Ps%qTqrz-IzT^6$d_N4i$Oiv71?ziWBThILqK zaMo2=N|&$Na#!=mbH4;gxDVj-vCC1u^_Iw60FenG96G&XS<TY&?OPV22B|1twgMX@ z-$~NJ0pw<E$Xu~{<K{&>7BCbD0I>X3YnRmg^H0Jq0A;oKzPe`fI`T3@waI3bx2>sJ zy=?xh+G7Ccj$prX-P#QsAHQ(VRe)w}GyWlN0vh;o`m@V7FG?8QEv4*z90x65?5K#B z-yZy@ST;-`Bgm7DzaryE<GWNz$gzfF?C!<_jw1>aF181)D1kkVWB4J2J$CuvPH9%d z4xxy4e%JWEFz#B@EsoCbJPJ>-XQAg<_r2X)1Ah*<W3hBwdRDs4HZ|Rzo^1O7AYCgi zXl=D#u%+2XS!Y;3wY_5fz-qH5TE|#RtS?)4Sq;{kEuEI9Est0qvfOPwY$>+90$@35 zIc`BqTE<&8Sk|PymxdNk+l}w}Y0stxQ`e;KN_8}fQ>T_=2P&PKocdtu!ql;;3sP@Q z`DJQ0KuShR20pV=%0#)8q!g_=lu~b=ZZ?}!%*V~tJj=YrJl33%{8sXV0H%@2g#gJ0 z(`nN$jC+kyag1rNaj$8zaVOT6iSNd<#xIR6#_h(rrh}$u9VWf$4dY2;3U=#_lT9m4 zcNo4ld|_N=e9}1Gc;4`~q1AY&aWsJ8d!yNyY@~)avHlBVhT(H#mSMBOV>Ied8P*%d z>Fe}S{Q`r<FiF>{>(r0bKc}Cr+o4U;e5PHm{UTAWSt2g3wV|JVuT4tm(IhAQE#Wbk z44S9V8`TMSCOm{)Z3*E7X+n+qBlUd=$@nfL+>tO*{So?-OmVMdm2`jHJH(l;CGsd9 z_2Xn$L%Jfr;dR2zA$Rrmki~r>C~$jk9?)_NxeeQ{<?HQYjZBL_r^KI=NOeQH+>%X7 z;;+NRXKx~TWa#xe4$C9?4Syg8N=!H+O%*r(H345-y+XOd@gaPSZRqF&(tB7JvzXrL zE&88$Z?B~n`zlyglJR5?UcbBX3XCF?$OL$TXo;Grh!TDee}M1d9Gr({GMwB5Ps1`O zh2gLSo(Jz;#0KBMW%!mz;5%r7)9@vH1!Lhj`~#99AMhFx3Vz5VTj3~7fkt=(0?<wF zBfo-T!pKJQAQZu9vJJKn6ETpLa2QhIDwzpxvI@7Qh1u{&c%`9G`H5qd`KZ~GGA`v8 zDIcc<QfH^`NIjdTPMe!{f7+q6FDypOO_m*&k1Szpj&+swch+WGl5K(QH@5$4bEOYS zuTKA6`X_eIo?~BZciK;9$TH?;I5Li9bY%|CoRhgT^PS94RzcQ=ti4$+*@@X(vj3W+ z${CsS+nh7G^K$p+_6#Z;wA(SLJuf$JM&84DujaW1YX&bE{F}ky{8{<W=AR!jXvl3t zULWEbI&SEFLz@cn3#J#`UhqJ{(*>srNnuIh{KAI9KNWsc7%Ivt8dY>}(eI1?I;?2e z%3-I9)x}ea*B5_M!b%R6jxD{v^tICO$|Pm^Wt+<mmt7t{bNJiCKO25-_|<YK*Ooi% z<;CR_$`_TtQQlM@9HALeJYw32nh{$@JU-&J5nqjDBlAa28@XxZ!I9@iNk@$vwQ1Db zqcx-FkFFd2X$7koTv1U`RdG+na}}>wyjyXy!Zjvs%&alaF;9(Y9CM=5Ryn(JcjcMN z$k@cO?~SvJyLsG}ac_(}GhRKuZ2W`c58jl2)30xO(=kChp<u$?37aN7GQm4>)Wl~d zUYvCIq*o?=Jy|<>#N>x3e=x;9rE1E-DPK=DPOYB$=+rl-c23hytC_ZIx_$bp>4&B_ z&oIrXoH2jKlQTY=sh_!E=HZ!vS(&po&3b#*r7Eb(tQuK0xoSz(rm82Z_E&vg^?g;e zT2Y-|U0Z!(_D!>&pWQykVVW~z&SP_q%v~_|m79m({QNxCyy|)H+@iW==`D}m;=i@v z)<<uB=QitY^KSd{XIVcxJYO~c4-4cAoC^*u2;821`@Y+~cU0W5@{XemVc~rXKVM{D zv~<x|cNX8d@6L-g_L@6tnir2+{J`RWEwL<Fw0s}%;)S9C#0drh$Uq4R0F5gXic1^} zHp6DNj{O#-P=L1pvT}xJlosa<H|AuN78Vv|=+jFxu+(BM)@RtWOSL6s#l>Z1#ls9n z%z9f{Nw!_k8R&`fO25Cdd|z&Lb#z_zv19241?dMAW3zKB6)&V0L|-x*?MCC~5tY?r z%lEn6n>R;JR2QUcXkmIm#*iTy1?gh9-PridO=%T_2`apg*=PW@f-IuV(5CCt32luQ zR+Go00dis=If2FF)zQMnx11xA90F8BHE!b`yluHqf;V<HV1UcA52d9gW2mUCV4=eW zgVA8n>(rDDD=RI@&aex5ok237&JU<#`|#}S9Pyu;qFfT1pqv%qGyab>&E2`Ga^#Ag zx9am9Mww;w+<fy*;|Zx;wR1;uMn*E`;nzx%rZpP1n3r#TS2k>feC8Te^_JO#{_;RI z3eTPX_&pOjen@qxE@9Lvsz_4Id17@XX^PK)vmEQI<i@AKY4*KXXP8l-STmE3saZel zfxnOm4v`T3y9NILc0X`?Kf1foz1#bL+fJjqp=e5D#@RfEbY<3gJCo9#w1MuTPd2Xm zPR46BOV+O0xNObFMeAxdY+SZz`P#-UmjZ%(&D!-FS1h`B*@lgc>pE6am9S*(+Pi<g zOc5^{Pr3if8xRahEQZdGC8*<$0c2yttG-d>$%f0m*^VU``<KJd;ci$BYvCT)0Kb5H zVGH~c?nnMs3-#~-JOsanN8mTG0~+8-*bTo!#CSH2BF`aW9DtYL6%6UGArLfTuzwTY zhQGkO2o4{>NAOql<4@z@aRUAY5k#OwqC;3P5i?08miZ)|WRh%>O9qo6q<|EWVp58r zB!X52sU#B+x~7upWG1N=;fx?ClSO1PSxQ!rm1GrJL)MY?h&h|cX0lbpAmSu-WE**q zY;Q;m6g5l?WRhKtD+5|8TS0b-)Id_$xGne?alC@Id1EM$Lna_^odWWgmw~*07m$xe z137jU$nnR4{8J6&i)VoR>jfZRwF3Fp3FQ1FAm85!<l>7!E?)rBF&2o&2E_XyvfDjC zf?l+EK2Wk9DE~fCX(~{~TR>I61*+W(G-(r1<HO*f$)|v(l>oH_fZ9(3&D;qzFB|BP z6+nkA23o2CIs%19eGGJLBGB>EfKHkNbP7&11Ldo5^0}`9op&7Q{5?SLm<4p%Q$SbZ zV(apNu0H~F%SoX3zXx<XzW>Gov;pOJ<2rjkbpU-11rKZl`jQjqAIAcH4JT++13ioy zzq1wSyJvwOvjhEX70^>CcLwFZMFr0XfL_c5+WG>}E7;bl0~-7gXzWp7)B=og!1xQm zBzENPxxmz?fhGMJnBE7>ybf5Z37FNP1ZKMoEb~5KS$lxx-HaasyMPVFNpRP!A`jT4 zp}?ko4Q$qfz^ZZZZ6&}Kd<bm07uecqz&4=lrXaAbD}epd4eb7{!0O)t_R#CV9?l2$ z#CBl6rI<T_{q8KVJ%0oC^dVr+>;dL@b{nw0cLDqT3}E|k(&tgb7vBQ*(m#N`d=l6} z+`+46V6V*q_WHxX-awf~T<~xJ*jr11y@SdhIRoryCa@3j{o`Z6K0*0UalX%R%n4Nb z)P7)RFn^88o_hgU^JBoiM+my`CvdQf*m21T>~c2dmw~n8Ze5AMuG9f@w*vFd0_Mj_ z10BGE24JDFz|lv6<Ie&o-v?YmCUA)-flGQBxC|+9gPgz(T?|~|Xy8iU2X43vxY0Of z4Av(bfSbA>xVd)VZVdxBf4>8`#h(JVY$<TdQRwH`xY7;W1}rzD-~+9|p$WOiaPo%R zfqRMrZqJ{Ad$t_7{d0gjum-p{UI*^b8Q|VS*$)E1eX<p}Pmcrl`FP+?nt(fv`HT&? za~}iO{3>v+Cg3_Z0_Q6OuE&8Ihh_j5c@MbQHsCo6@WL73@tW}3bHJOFz^6HZw@m{+ z2ipd}1AM_#z!$y@{IEH|mt<r95cskd;D?_Cegu{y&jLTjfEmlNlQ91Y_;JU9pS%?K z8HK>lT@U=v-U5C}BJhr7&j7#TQQ+5_fWPM$@U?2-9~=$*juXJ|#>MyHg8NSa|0+&) z$Oilo9q{j=LdWF5|9uzmU%UwX=@r1A$M)~A{mORWJ+A^EodN=90wJLe1pN^ZGB$yb zGZuvWg&-7gAdFlJLgjj>5+;Q~n7R*y*>(`-VdFwM2#XhkutE*O-P=LfSOUTp>~rh^ zVVe|$hfw~J_d$3vAB3m>2Esm+eSR|tFTDuDA76Oj7&&~5Dh}YiDuGG(S-aH6!2%n= ze48BB+9*`n2&}WoV6{zv8k-dRdAQA{z>?Y10XaSLdO~^1^OPZtzn1zdPMbu<t0d49 zpiyapG%-=Clrso*cLh2kp-_bJJWH3v_>dz6J#aN32}l#AfsT$2m&=9E07L^mUmyxL z>y1nGt<$@__-0Ka-9k4>$`XWNPzZK3Vd@B8^%15_5)y4TLYqA;wdO9qi&tJyLQ9Jm zl&0+L>_NG?gRod_QmWHZ`xYS*DiL?nzYUI3>P+Edg)tu7tpTaVrZJf`IxPpmj&^r6 zOu1B3qLSkx0jbpO7W>?-trt**3$3m0%dH*h80iO?YPYSD>KtlyYK|!<RSmk_Gp>AB zSG~N;m7t&y_PTYdI;WyD+}i57;5nfirgPP@E|*UAFjeVv;FM;LF`LI^!Z*k3B(A38 z$4g3Vx_XPwW;WY$OEBfy%8NA6@sCfM&Ky4;_W8n`T+Y=iv%qX_X^DLU-+a^Lsw0lv z+&boJvgztl;$uYN<L@3kc<@~;&op^ETAG?#I=uDCh;&pu<^iW1KXpb-Mrx$gINBTH zl}6>cbIKMM@JVJbM*ri@&9zDTPQCYnS9wn9wcE3Da<c4pFU!ohW(vo(3FC8#(~HO0 zo3QG%M}pB{1UN26!aZR>rk-#xgb*R&Br+LcEEbJsL`ak|WN<Pr!i7;l?2<|4aw!%P z4hY9{B*T7#?faM6ZO-T94oWz?AB2bi{*Fk5u?UlEFv*yVC$XSE;&;1a5Cf`_sA|(B zQAsqY4uVQ0K+t>T%9VDPs~rn(um_XXa^pe+{3q^6oHOm;_7n~}Lb3RkWRx))wcNO$ zFo)Za%jNit@8v(Pfw&jU@hwq886{S4OcIvFylvqy$Avi^CQdHIVgYa1<%(}4Nv5ea zOC!=qkG2Oi7*BgT+uB+#UAolP*4Y#A2mJr_uJs?0+UL4>^`grMo(rk!X0@zdCR6(Y zF_0-de)L-f(`w~9x!v=vM_tQgYLEZd)bH_#cVK`Y^`67@{z-8w9HBu7)*6GIjKm_r z2<}@(A`%{Ap&*L|0`*iSmDZ+&xDb=D2zH5$7$7i21OhSk&}Rqeo1YzEA4VN&jAUx` z3pL<xN*KwIo?t0bSs7*lvlKIrZ3@gx;6>Fa;ky)i1?S>*5ueMa;?=>XsM5wad8tkC z@E4_;R@TJZA|`hr1*qEPs-@xZxou8r=eSy`(|PK7H%8I3$9YFdCURE|I3+68`SU8( z5L>;Z>5Hze^z^Q-ZM-Dp3q|F;kMeRp$oDV}3A%ZW5Od>JZ|w7PI?{vcNMf$A45&}7 zu0=mF;gYS9NG<hv<n_2lBqEhYBHe*{iB!Q@Aix;e_U6Mca9`|oB*O}f>PhgdO#+W$ zWOra>--Y%#jFJ65w8tPYAh|bUsfIlKjb#v+u++gGjQG1S;y;HG|9On~wP>eJHimtA z%n2x`!7>rE3bPW9?wlC=W%T{{Ut4S>Yh;>VO@P0bz?aC}$CBZ&wf^W6<D*BSM`HJq z@X(2{50AT&RDc`m*u7K2E#qQKqetqChanZ*tsTuJVQ^pr(m*f4k<KEmPG?Hi;++rr zJ6l^iqpr)IU=(6eLi=*W=L>~ngi~tNc!d}Y(<wp9{O#@SSFZX3-gZ|Ym|;ye==ulX zK)Jv{H0tl}ia4ZlVDj+LA;95!xO_hph;IWlV;pw%`Z9JnGce{~^27v*RF!1ZrP$JK zHd`t>Tri%c5MTnwYK3Sx7{uVFX$peEXtCMSj9QIW#i1XWgB_x*LwtxI@j;+WV4Oq% zybyzO@dKfMvA9uq+@T8f8PFQn(3vAwX*9YdStt^TL_=P$&+UddQUU7;M1tW6;q)m< zQYlz8Dx?rB)YaA1>2~`ffgs@n$-p71Nu?NgVzrW>zek#+flyH)CA*&(%ybUib#GFO ztKE=*)T?=!B-Ya%=yJ7nkN_cd$$jSnx+J|;BI7w(f}{sQ-cOu7At03rwK;@J<Xn&2 zg@GJx(|42@wggV1P$cQ}X|^QryU3`K;J`tcu5TKZ5rYdh;Nyn!hJ(W~&JkvFsyTtV zzHe`BI&-$e-`e6vj09dT=l{JlCF>JST8-JN3AVJf_+TUw^^m)zrM0ZR-Rrx0?wq?V zG5J4-^-dGhY7$e83CU@L1}B5Bh0o1_-GwbokB~#Mu4|4eF=sYfs;w&~l^RpbT7@h@ zpP)AwjRvh0np<2~JG$JisK#hCMswO+U7$`j8w|Qcb%HiYtI`;fWP<-{n~``>>T27i z?_41s!bf)|Fm!vhM#+&_ID|nmf;Uj3R!caCT1ospAxb%kRIaW+x|5HfGlrve=w=)* z2uu`=M1aZU5<w;vcn%>JN8^lAsWd4?&yh$t7zy;m1yw33uQ#cp;TX?zyiBbUN5_14 z%>sg2$~dW@(IqB{jqUBeSfVvm=SWJF%LT?q+r>G3XxN~}YSw5?iTY%dT$F5X?hNU& zGfg^qqEV$#2Ajo3s!Ya|2pyhocV|cI_dc;PB}L82V(ymqD_w4{-^Zua4=d`sBG*ma zTkG>!hv>N64Hw2)7(@$sy@4p@QDvn{O5_R!Qa~Vx{ns+#DS>D(&=U$FXczecK?ub- zg+d`mcjgsHLAtNHz1{utI4%?=zzG7v1VR`sEQl-OzyrVLsJ$-Sr&#<##eKS8^=z3M z$%sl42}i;`-G28~x7*{3#3F==S>&D3NHiLa)e7-=Nx`m`Q3!UsQO+9(^h8ksoFXob zBg8w+9JE#*6NiPP7+dgHK8(4j$aS*Yd#^+t2mUc$K(Ri}n6ucA7VGj89vPWZCAnrW zaiSQe{xT+h8T8o=N_;^;%EOCBLuXUSITpFviAK9VpmzciilQNTK|;eJpk<}l+PAbg zHBtpJ3g4h0DvK5rrQ%l}D{LsQK%hvFLL?FjheG~t7ZSGHy|J(e4C2M;Eu$gILo^bO zM(Y^mI4IR=70~0kg7FDILp(lTG(tdgUA}i5kHa{QBItzibRs|p=Ut1At2v5Dt`YUT zo*2aH)#08H;FZ85QN{RJtS%`=Vgx*0o!y~eBogcn@oII<i9LWfvd$I@1)`injr;`H zLCKg}O>nJ1pv&WNwRJ%(JByIrMJ+7J!SJ`R{v;Y9i!cE%GvXPN(@;cIG87Dk!cqx( zE7~5y_!=Sjy~H3K6rCDp4vP<TZ;IdTjKiZ9Bs@XnAxEE3#t*=E<w^ighSm}i6fYoR zOQh{WXUq`|BLnvP{P8eDq*|?38tVz;270>OonBE?rQ#!3S}wb<ba;I|p&-FXu`@wD zvhLOkSN%N!G=DT2zv*bx*5)f1g1WnWUQZ7d8$~ON{2%o}rp9<8HU<KrP?s;p#bOS0 zARy6z*h*AsX=(EAw$6aRv-|SJOP9q)t2KeOp8o3MrS{IN{$PN!ijC0_az|HNw^D)M z41z2nBsO+;2P18lLkbCx(N7{ox{+gvU&pJFLoG)RRfQaCA#$h(kVB2ZOD=vLFE`l) zBveu)SUk)|vNQ(&!vH@#evr*L(|?@3*na|BK|=jw<iu%>^AX4YLP&mqgQ$8rNFN2k z4?r;x^e3$Lu@0y4zp)I{KvtpcqZM&hVYU1Ri})eVAnD@_{R~0>!wg|?9{hAV;2d-v z{vAJt|9O3Q{}bTenGzB~JdWb38(>_-@dK%C2;HGx?Ct|^M=yF4gzp3RS$xgN*Dc*! z8wd|h`%eI>jGqEc={8Cbi7@?lFtYt9`q+LHZjkl-S9n1tbp4O<&g`EEDLIHDKM6zz zi81lIJ@MrR+*sPbI!2+~AHs?sgo@ms3=b*h>z<B?@Iwf|L<H!M_?rKS_|BAg9FGU` t$iPT0#`5TaR58YrYXN*<1jiW72?yeVYZOQxS~`)(EWEjNqGbQP{{c*m{x<*s delta 3386 zcmYjU3tWs@8~>m8yzg6x={hr}X=b{rluD)hRrfNwOG2$A328&y`V4j#OTW;x7HhHA z($|KzvEf^{#iq3``?dJm+zGj?CCe|dd}msFdFS_>=REi4ocH|C%xvsZEpJh|f&j2! z0}u1~jI8KSSAI4Oc(w%~TNARQ(#cjh1HCb8TvfW>GUj`{U|vBv%<eV;&;6L3I%Q@- z+5e6fGA}_1m{=Dv`^qOXcQO0>%>H0n!P~|-bCz`-<EGO}-zl>Ea8&>x$plWFoW}g8 zr=Bwd`pO_~SzA1{pfGUx<Z{*l%ZE|LOkr}v#2z{j767N>nH7~2znCzXIrIY5sdV;~ zf>*Og{Ro(O8|9e=m1Sf*Wet%^=C7GmFmr0Ic5xeUNyF@BWwYO|F#Gvf2XNH`kd^dr z0oQwe&A`N4lO>@~O*wjm#o_i(tHS#E<=R_Du8$!>WQ}1Vi6Z)^oVETu_e_79mV6$< zlR?Z(>ew!tpw18{coO}5G0Sj9WS;XyCw+jakFIA>k8jVUp0=m+pH}s@^geq1+FogI zZ?DuO*sJVqG=~9uwjw~?t?tq&H9_hW_4k?`>ceWa+EN{+_Ep!bYt=UD4Ao=RC#sd| z_f#{~U#Wam4S?!L)k#$g<58;FsuH_{cBO#bS|+>NZB#y2&Qj`?&1~}kWgs(HGd@(g zM477$RgP0;*j6bW09yxJ<LGE>e50_nu$3tKY;}rcg+gJgXj2eHiej!JRG}W)GIR+b z4<5=kZm6ZaU0xyEB>UYMBi|_7B#)ETve^`)EW0Q>C%YwEF3XVD%h$`Tbn>0D|H^Ec z*+LdCFOrX!c1nMe&5+f}l4KX9`=xhelVrhw^oC3>vynl{)+4Qusimi7_R{&%hcYW^ zgw0}`mo~Xlg*4i_+xn4Bkj-YBMC(-+R_3QI=2)Dy6wUh!Td+eTxoIJhJU5q1S|v5? z<gupABL++ICCiwpThb>nmE;dOI%I)F&gAius3C0g&4fX7O-fC-^dAP}0^U4iDSNXk zNR8fS;Bb8t5z<Ni9~P4Lrwen){)7IZNhRq__lEw~8)qb@e~j)QBS{iVt#TnF`bD;{ ze?G*Cxc)t7VNS$F{~3{T#F9nW4etHZ$oQi-?V0M1;3$(@|6v0TFv&>9XWt*}ud(sX zto|<z5JAF;mZYMD_>%w<O(L<HNQfC3L<V9Lw%`UX;|i`3UlM^0D1<)(@D{eAhbRlt ziEdmX0<PmK&f^?95Q!gf8bjd$GrVRPc?x$j4~LL|W;Ed$o{&mX1z$o)8Ce1!gpx(5 zBvN8cim(St{6Z4(kQCztERcpqH0V>!PUz+<8WhiMJ#81*HYo=v<CS&FR=YuV{&uBy zEA7tOy->xg-c{{UT~#}&)70zLKWMBpv6{u2ZJJy5ruJ#}I{O3mR~-}%fetke-#QL* z3~*fJ_@(1br-4pMPUTJpr#sHWowJ=cIiGa#aru|acP>4{QirW}9qgLt`jzWLx3O-Y z>)fun^X}2^Mef_&+lE^Wj~%|pL-fe<_}JsRr@3db=QhvhBm73J8`0%Oy{x^$yfVD1 zz4m)O@V4|$_Ac?(dmFqjdq45<@)_szFQ2<39Y$7<Z1sh&yRXjo8^2P&UVnf8Y5p7h z5BqnG@*XvF)YbqYU{1gX0c!(x1ndj=KA<x|cR%1|pe%5D;JU!(!1I9*f@qL!(8!>) zpm&452s#t=AXpx(4PF%75PUO49x@@MIpk%iQ)p@E7on#^uZBJjvkHq1OAnhIRuNVc zb~x-&xI=hgcvg6M_|fnu5kV1E5t}3SMCL~xiF_7i8<i1N71b2g5$zhSjb0FaI{LTK z^3fr>(W#^N#~`L4rXkilwk&pY?1k7@abx3_#@&gxj-M01IsQ_DO+s42f`pcY3yCg? z;}h>B`6RuQv_0u^GM}tY&P`sP+?k?DsY*GRa(7I~n1y2+$9y;D?wHqFGp(yOM4PRh zuC3G7YmaGv)ZQK&H}<1c)6|mGrqs)67HJu2i_;4<>1F9JGfFbrGi@?;neAC_S!r1Z zvVO}>%hqLgjZ=)<Fz#MXRZjc(qVZ>QV{_lhJ(zoc!qf@-^SC^(y!Z3In;12*ZsMIu zZj&}m>YY4wa>L|1`40Kp3s8_$u(9CgTTVq&4GScReuk_VWtid9#er6aRcpNy3<s}x zQ1%^g@45puSl=2T7)IRd6%9U*r*np#PYw$<1myK=NZ2hxFa)k(_bDmYFL@q9>hy=6 zr|I5ea4AADN>GYfC__0aFb|ce!U8PBA}qoCSc>IXfojyC7Ij#Qbqr)1`+>KWfowbK z(ZEo!i(#jk;iH8?`acZbhhT*MafXub`|)-b7x03C)|^-}BuR;!D2QD!(GUlAkqjel zWH|98Uc`r?*og8V5<(&w)?-OLNhB#o{F8K&NhXj<B%c(LB7JM0O5fe*qnEvKA*&7H zFD$qLIb^l*zBJJoa(=HNy2f0P-7z40*rHo5fP7O6a;O;OxGBhq10bh@LC&;+w6}wt zw+88G1nDdXxl#>s?HtH$bCA2GAit!5{Mroiq!r|u2Ba?s<mDc4cmR$jfD^mGnH~pc zb`so>t>7$Mz}WygPM!}=;RH^_fm0s>=bR4CwGNzT4LI*maDL2r)FW^q;o!pNfs0xQ zZZu1j!2FX~a&0HL)Lw8|t>AL1z~vtWH<cBekqvHUH@J#d;41HddymP>lfcz5|5{dO z{WEZzx^&>SZUMKg5nO#KxCWMBS1`CH*74Wd!R<E%cT5YeZ5_C?%<m_bxPvu#ku`iR z72M5saNSJzAQIfuhv545f%}~mdF2gC%t1LVs7WU1Kn`@s4NwaQP@9dQvKUZBAgEF& z1y$FBYFa^^7l95t0_wI8l&zWuW`joMg2sxViOrxXEI1<{H2Wdw6b)#}JkVLpx7-DE zZXM`+C1~Y#(1o`_ms|q<AP2O152$_wXq^J|BU45_plh#ze%uPWt_f7Pz7}*tCFmzC z=@!=E=Ut%N`apM>f!0TZ{<{jafffJq9_UV%tcewFt_J<u5_B&>_osj!VDh0ypx-k8 z;}xJMSj@>?pl42jwlg|c4%%@B^kNI><!;a`ouF5l{(3!hv`Y)x)ed@#t@d^>=$!`8 z`)unTvei9iNuNkTe+vhFS`5k#6mR_SZQuu_fH&&{Ke!#doi}*Ldho6*z`GZM_v!{e z(i;4zV(@`%9u*FL^eOOh72vfSz^D0v&oBp{bxH?5`v~~S%vi{bbM}Fs`x1Or6?mNo z_{HbIFI@`$LsRgp9l+~ZqE8or-%=00f%)!i18+zI-?9<>H<sWJvSQydI`#<s$vW_- z7lQ9-0)IIM{0$L&R|ohzO7M4Uz~9dV|EumH_#T$*c`^9kcR`?+APh)_V5WdzRSZG4 z0)lN3`;}e=LFE8J{S1Pp7J@?y1jnrqoEdj%gy41?f;;0LQbtQ4cy5H?>j#1TdK4m} zA;d0+puGVhtrS9L9)#R(2ou*q(3M_+P;Lg{-7OH<p%JQC!PVO!Y+%VYKZ3BM3qk{H z$j}L4Zy|)kl@N{wLO99vXPExy90*qyLbxY~@GDFDA{`>BhA4@HXk`vjJ_4fR97GpC zh{K~Hjywg?pZ#YD?SUBOhX^sT6QXt`#EdHt$K^oG=OGp{<BSrBvpXQZ%>w4HhPdb@ z#ARzCRx{r<2@pT3gt&g*vPMewHd1cTj%6!%b9-0rrUzc_P2SC)*|cm<p1?@lJ15WN H%dGzc+gh4i diff --git a/commonsUI/src/commonMain/composeResources/values/strings.xml b/commonsUI/src/commonMain/composeResources/values/strings.xml index b0fc2cb0e0..d1e8fac7c4 100644 --- a/commonsUI/src/commonMain/composeResources/values/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values/strings.xml @@ -5351,4 +5351,104 @@ <string name="cyberspace_bag_unsigned">Unsigned, so this author is a claim</string> <string name="cyberspace_bag_unknown_kind">An item of kind %1$d, which this client does not draw</string> <string name="cyberspace_bag_opaque">%1$d bytes of something this client does not read</string> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> + <string name="browser_pill_close">Close</string> + <string name="browser_pill_tile_copy">Copy link</string> + <string name="browser_pill_tile_find">Find</string> + <string name="browser_pill_tile_text">Text size</string> + <string name="browser_pill_tile_desktop">Desktop</string> + <string name="browser_pill_tile_home">Add to Home</string> + <string name="browser_pill_tile_other">Other browser</string> + <string name="browser_pill_tile_full">Full screen</string> + <string name="browser_pill_edit_address">Edit address</string> + <string name="browser_pill_back">Back</string> + <string name="browser_pill_forward">Forward</string> + <string name="browser_pill_reload">Reload</string> + <string name="browser_pill_stop">Stop loading</string> + <string name="browser_pill_favorite_add">Add to favorites</string> + <string name="browser_pill_favorite_remove">Remove from favorites</string> + <string name="browser_pill_share">Share</string> + <string name="browser_pill_copy">Copy link</string> + <string name="browser_pill_find">Find in page</string> + <string name="browser_pill_text_size">Text size</string> + <string name="browser_pill_add_home">Add to Home</string> + <string name="browser_pill_desktop">Desktop site</string> + <string name="browser_pill_other_browser">Open in browser</string> + <string name="browser_pill_full_screen">Full screen</string> + <string name="browser_pill_left_site">You left %1$s</string> + <string name="browser_pill_back_to_app">Back to app</string> + <string name="browser_pill_text_smaller">Smaller text</string> + <string name="browser_pill_text_larger">Larger text</string> + <string name="browser_pill_text_reset">Reset</string> + <string name="browser_pill_text_value">%1$d%</string> + <string name="browser_pill_privacy">Privacy</string> + <string name="browser_pill_tor_title">Onion routing</string> + <string name="browser_pill_tor_on">The site can't see your IP address</string> + <string name="browser_pill_tor_off">The site can see your IP address</string> + <string name="browser_pill_site_settings">Site settings</string> + <string name="browser_pill_site_settings_none">Nothing allowed yet</string> + <string name="browser_pill_access">What it can access</string> + <string name="browser_pill_access_desc">Your keys never leave Amethyst</string> + <string name="browser_pill_console">Console</string> + <string name="browser_pill_security_https">Secure connection</string> + <string name="browser_pill_security_http">Not secure</string> + <string name="browser_pill_security_tor">Onion-routed</string> + <string name="browser_pill_security_sandbox">Sandboxed app</string> + <string name="browser_pill_permission_camera">Camera</string> + <string name="browser_pill_permission_microphone">Microphone</string> + <string name="browser_pill_permission_location">Location</string> + <string name="browser_pill_decision_ask">Ask</string> + <string name="browser_pill_decision_allow">Allow</string> + <string name="browser_pill_decision_block">Block</string> + <string name="browser_pill_permission_state">%1$s %2$s</string> + <string name="browser_pill_decision_allowed">allowed</string> + <string name="browser_pill_decision_blocked">blocked</string> + <string name="browser_pill_address_hint">Search or enter address</string> + <string name="browser_pill_go">Go</string> + <string name="browser_pill_clear">Clear</string> + <string name="browser_pill_paste_go">Paste and go</string> + <string name="browser_pill_fill_in">Use this address</string> + <string name="browser_pill_find_hint">Find in page</string> + <string name="browser_pill_find_count">%1$d / %2$d</string> + <string name="browser_pill_find_none">No matches</string> + <string name="browser_pill_find_previous">Previous match</string> + <string name="browser_pill_find_next">Next match</string> + <string name="browser_pill_find_close">Close find in page</string> + <string name="browser_pill_console_all">All</string> + <string name="browser_pill_console_errors">Errors</string> + <string name="browser_pill_console_warnings">Warnings</string> + <string name="browser_pill_console_copy">Copy</string> + <string name="browser_pill_console_clear">Clear</string> + <string name="browser_pill_console_empty">Nothing logged yet</string> + <string name="browser_pill_perm_title">This site wants to use</string> + <string name="browser_pill_perm_camera_desc">See what your camera sees</string> + <string name="browser_pill_perm_microphone_desc">Hear what your microphone hears</string> + <string name="browser_pill_perm_location_desc">Know roughly where you are</string> + <string name="browser_pill_perm_tor_note">Calls can reveal your IP address even over Tor.</string> + <string name="browser_pill_perm_allow">Allow while visiting</string> + <string name="browser_pill_perm_once">Only this time</string> + <string name="browser_pill_perm_deny">Don't allow</string> + <string name="browser_pill_dialog_says">%1$s says</string> + <string name="browser_pill_dialog_generic">This page says</string> + <string name="browser_pill_dialog_block">Don't let this page show more dialogs</string> + <string name="browser_pill_dialog_answer">Your answer</string> + <string name="browser_pill_dialog_leave_title">Leave site?</string> + <string name="browser_pill_dialog_leave_message">Changes you made may not be saved.</string> + <string name="browser_pill_dialog_leave">Leave</string> + <string name="browser_pill_ok">OK</string> + <string name="browser_pill_cancel">Cancel</string> + <string name="browser_pill_info_connection">Connection</string> + <string name="browser_pill_info_https">Connection is encrypted</string> + <string name="browser_pill_info_https_desc">Information you send can't be read on the way.</string> + <string name="browser_pill_info_http">Connection is not encrypted</string> + <string name="browser_pill_info_http_desc">Don't enter passwords or payment details.</string> + <string name="browser_pill_info_tor">Onion-routed over Tor</string> + <string name="browser_pill_info_open">Open web</string> + <string name="browser_pill_info_certificate">Certificate</string> + <string name="browser_pill_info_certificate_desc">Issued to %1$s by %2$s · valid until %3$s</string> + <string name="browser_pill_info_permissions">Permissions</string> + <string name="browser_pill_info_site_data">Cookies and site data</string> + <string name="browser_pill_info_site_data_desc">Logins and preferences this site saved for this account</string> + <string name="browser_pill_info_clear">Clear site data</string> + <string name="browser_pill_info_clear_confirm">Sign out of this site and delete its data?</string> </resources> diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/AddressEditor.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/AddressEditor.kt new file mode 100644 index 0000000000..f46a97d35e --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/AddressEditor.kt @@ -0,0 +1,219 @@ +/* + * 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.commons.browser.ui.pill + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.browser_pill_address_hint +import com.vitorpamplona.amethyst.commons.resources.browser_pill_clear +import com.vitorpamplona.amethyst.commons.resources.browser_pill_fill_in +import com.vitorpamplona.amethyst.commons.resources.browser_pill_go +import com.vitorpamplona.amethyst.commons.resources.browser_pill_paste_go +import com.vitorpamplona.amethyst.commons.ui.stringRes + +/** + * The address as a first-class input, opened from the origin field. A 56dp pill field with the URL + * pre-selected, a leading search/security icon, and trailing clear + a filled Go; below it Paste and go + * (when the clipboard holds a URL) and the omnibox suggestions, each with a ↖ that fills its address in + * without leaving. [onCancel] folds it back into the origin field. + */ +@Composable +fun AddressEditor( + initialUrl: String, + security: BrowserChrome.Security, + suggestions: List<AddressSuggestion>, + clipboardUrl: String?, + onGo: (String) -> Unit, + onCancel: () -> Unit, + modifier: Modifier = Modifier, + autoFocus: Boolean = true, +) { + var field by remember { mutableStateOf(TextFieldValue(initialUrl, TextRange(0, initialUrl.length))) } + val focus = remember { FocusRequester() } + if (autoFocus) LaunchedEffect(Unit) { runCatching { focus.requestFocus() } } + + fun go(text: String = field.text) { + text.trim().takeIf { it.isNotEmpty() }?.let(onGo) + } + + Column(modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(10.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + IconButton(onClick = onCancel) { + Icon(MaterialSymbols.AutoMirrored.ArrowBack, contentDescription = null) + } + Row( + Modifier + .weight(1f) + .height(56.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHighest) + .border(2.dp, MaterialTheme.colorScheme.primary, CircleShape) + .padding(start = 16.dp, end = 6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + // The field shows what it will do: the page's badge while it still holds the page's URL, + // a search glyph once the user types something else. + if (field.text == initialUrl) { + SecurityIcon(security, size = 20.dp) + } else { + Icon(MaterialSymbols.Search, contentDescription = null, modifier = Modifier.size(20.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant) + } + Spacer(Modifier.width(10.dp)) + Box(Modifier.weight(1f), contentAlignment = Alignment.CenterStart) { + if (field.text.isEmpty()) { + Text(stringRes(Res.string.browser_pill_address_hint), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1) + } + BasicTextField( + value = field, + onValueChange = { field = it }, + singleLine = true, + textStyle = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface), + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.None, + autoCorrectEnabled = false, + keyboardType = KeyboardType.Uri, + imeAction = ImeAction.Go, + ), + keyboardActions = KeyboardActions(onGo = { go() }), + modifier = Modifier.fillMaxWidth().focusRequester(focus), + ) + } + if (field.text.isNotEmpty()) { + IconButton(onClick = { field = TextFieldValue("") }) { + Icon(MaterialSymbols.Cancel, contentDescription = stringRes(Res.string.browser_pill_clear), modifier = Modifier.size(20.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + FilledIconButton(onClick = { go() }, enabled = field.text.isNotBlank(), modifier = Modifier.size(44.dp)) { + Icon(MaterialSymbols.AutoMirrored.ArrowForward, contentDescription = stringRes(Res.string.browser_pill_go), modifier = Modifier.size(22.dp)) + } + } + } + + if (clipboardUrl != null && clipboardUrl != field.text) { + AssistChip( + onClick = { go(clipboardUrl) }, + label = { Text(stringRes(Res.string.browser_pill_paste_go) + " · " + BrowserChrome.displayHost(clipboardUrl), maxLines = 1, overflow = TextOverflow.Ellipsis) }, + leadingIcon = { Icon(MaterialSymbols.ContentPasteGo, contentDescription = null, modifier = Modifier.size(AssistChipDefaults.IconSize)) }, + modifier = Modifier.padding(start = 48.dp), + ) + } + + if (suggestions.isNotEmpty()) { + Column(Modifier.padding(start = 4.dp)) { + suggestions.take(MAX_SUGGESTIONS).forEach { suggestion -> + SuggestionRow( + suggestion = suggestion, + onOpen = { go(suggestion.url) }, + onFill = { field = TextFieldValue(suggestion.url, TextRange(suggestion.url.length)) }, + ) + } + } + } + } +} + +private const val MAX_SUGGESTIONS = 5 + +@Composable +private fun SuggestionRow( + suggestion: AddressSuggestion, + onOpen: () -> Unit, + onFill: () -> Unit, +) { + Row( + Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .clickable(onClick = onOpen) + .padding(start = 8.dp, top = 6.dp, bottom = 6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box { + SiteMonogram(suggestion.title.ifBlank { BrowserChrome.displayHost(suggestion.url) }, size = 32.dp) + if (suggestion.isFavorite) { + Box( + Modifier + .align(Alignment.BottomEnd) + .size(14.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surface), + contentAlignment = Alignment.Center, + ) { + Icon(MaterialSymbols.Star, contentDescription = null, modifier = Modifier.size(11.dp), tint = MaterialTheme.colorScheme.primary, filled = true) + } + } + } + Spacer(Modifier.width(14.dp)) + Column(Modifier.weight(1f)) { + Text(suggestion.title.ifBlank { BrowserChrome.displayHost(suggestion.url) }, style = MaterialTheme.typography.bodyLarge, maxLines = 1, overflow = TextOverflow.Ellipsis) + Text(suggestion.url.removePrefix("https://"), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis) + } + IconButton(onClick = onFill) { + Icon(MaterialSymbols.NorthWest, contentDescription = stringRes(Res.string.browser_pill_fill_in), modifier = Modifier.size(20.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant) + } + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPill.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPill.kt new file mode 100644 index 0000000000..e7bd6ca2a3 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPill.kt @@ -0,0 +1,503 @@ +/* + * 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.commons.browser.ui.pill + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.Action +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.SectionKind +import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.browser_pill_access_desc +import com.vitorpamplona.amethyst.commons.resources.browser_pill_back_to_app +import com.vitorpamplona.amethyst.commons.resources.browser_pill_close +import com.vitorpamplona.amethyst.commons.resources.browser_pill_decision_allowed +import com.vitorpamplona.amethyst.commons.resources.browser_pill_decision_blocked +import com.vitorpamplona.amethyst.commons.resources.browser_pill_left_site +import com.vitorpamplona.amethyst.commons.resources.browser_pill_permission_state +import com.vitorpamplona.amethyst.commons.resources.browser_pill_privacy +import com.vitorpamplona.amethyst.commons.resources.browser_pill_site_settings_none +import com.vitorpamplona.amethyst.commons.resources.browser_pill_text_larger +import com.vitorpamplona.amethyst.commons.resources.browser_pill_text_reset +import com.vitorpamplona.amethyst.commons.resources.browser_pill_text_smaller +import com.vitorpamplona.amethyst.commons.resources.browser_pill_text_value +import com.vitorpamplona.amethyst.commons.resources.browser_pill_tor_off +import com.vitorpamplona.amethyst.commons.resources.browser_pill_tor_on +import com.vitorpamplona.amethyst.commons.ui.stringRes + +/** + * The redesigned browser pill (see `amethyst/plans/2026-09-26-browser-ui-review.md`): a grabber at the + * top centre that shows the page's state even collapsed, and, pulled down, a Chrome-PWA-style menu — + * header, navigation capsule, page-action tiles, a privacy card and the console row. + * + * Stateless apart from which inline panel (address editor, text size) is open. Which controls appear is + * [BrowserChrome]'s decision, so this renders web, nsite and napplet surfaces alike. + */ +@Composable +fun BrowserPill( + ui: BrowserPillUi, + expanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + onEvent: (BrowserPillEvent) -> Unit, + modifier: Modifier = Modifier, + showClose: Boolean = false, + suggestions: List<AddressSuggestion> = emptyList(), + clipboardUrl: String? = null, + initiallyEditing: Boolean = false, + initiallyTextSizeOpen: Boolean = false, +) { + Column(modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + AnimatedVisibility( + visible = expanded, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut(), + ) { + BrowserPillSheet( + ui = ui, + onEvent = { event -> + // Everything but in-sheet adjustments (text size) finishes the interaction. + if (event !is BrowserPillEvent.TextZoom) onExpandedChange(false) + onEvent(event) + }, + showClose = showClose, + suggestions = suggestions, + clipboardUrl = clipboardUrl, + initiallyEditing = initiallyEditing, + initiallyTextSizeOpen = initiallyTextSizeOpen, + ) + } + PillHandle(ui, expanded, onExpandedChange) + } +} + +/** + * The collapsed grabber. It tells the page's story before it's opened: the bar takes the error colour on + * plain HTTP and the Tor accent when onion-routed, a hairline fills while the page loads, and a dot + * appears when the console has errors. + */ +@Composable +fun PillHandle( + ui: BrowserPillUi, + expanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + val barColor = + when (ui.security) { + BrowserChrome.Security.HTTP -> MaterialTheme.colorScheme.error + BrowserChrome.Security.TOR -> MaterialTheme.colorScheme.tertiary + else -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + } + Box( + modifier + .clip(RoundedCornerShape(bottomStart = 14.dp, bottomEnd = 14.dp)) + .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.72f)) + .clickable { onExpandedChange(!expanded) } + .draggable( + orientation = Orientation.Vertical, + state = + rememberDraggableState { delta -> + if (delta > 1f) { + onExpandedChange(true) + } else if (delta < -1f) { + onExpandedChange(false) + } + }, + ).padding(horizontal = 18.dp, vertical = 8.dp), + contentAlignment = Alignment.Center, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box( + Modifier + .width(36.dp) + .height(5.dp) + .clip(CircleShape) + .background(barColor), + ) + val progress = ui.loadProgress + if (progress != null) { + Spacer(Modifier.height(3.dp)) + LinearProgressIndicator( + progress = { progress }, + modifier = Modifier.width(36.dp).height(2.dp).clip(CircleShape), + trackColor = Color.Transparent, + drawStopIndicator = {}, + ) + } + } + if (ui.consoleErrors > 0) { + Box( + Modifier + .align(Alignment.CenterEnd) + .padding(start = 44.dp) + .size(7.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.error), + ) + } + } +} + +/** The expanded menu (without the grabber), for hosts that place it themselves. */ +@Composable +fun BrowserPillSheet( + ui: BrowserPillUi, + onEvent: (BrowserPillEvent) -> Unit, + modifier: Modifier = Modifier, + showClose: Boolean = false, + suggestions: List<AddressSuggestion> = emptyList(), + clipboardUrl: String? = null, + initiallyEditing: Boolean = false, + initiallyTextSizeOpen: Boolean = false, +) { + var editing by rememberSaveable { mutableStateOf(initiallyEditing) } + var textSizeOpen by rememberSaveable { mutableStateOf(initiallyTextSizeOpen) } + val sections = remember(ui.chrome) { BrowserChrome.sections(ui.chrome) } + val page = sections.firstOrNull { it.kind == SectionKind.PAGE }?.actions.orEmpty() + val privacy = sections.firstOrNull { it.kind == SectionKind.PRIVACY }?.actions.orEmpty() + val developer = sections.firstOrNull { it.kind == SectionKind.DEVELOPER }?.actions.orEmpty() + + Surface( + modifier = modifier.fillMaxWidth(), + shape = PillDefaults.SheetShape, + color = MaterialTheme.colorScheme.surface, + tonalElevation = 0.dp, + shadowElevation = 8.dp, + // On the black dark theme a shadow doesn't show; a hairline keeps the sheet's edge off the page. + border = PillDefaults.hairline(), + ) { + Column( + Modifier + .verticalScroll(rememberScrollState()) + .padding(start = PillDefaults.SheetPadding, end = PillDefaults.SheetPadding, top = 12.dp, bottom = 12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + if (editing) { + AddressEditor( + initialUrl = ui.chrome.url, + security = ui.security, + suggestions = suggestions, + clipboardUrl = clipboardUrl, + onGo = { onEvent(BrowserPillEvent.Navigate(it)) }, + onCancel = { editing = false }, + ) + } else { + PillHeader(ui, showClose, onClose = { onEvent(BrowserPillEvent.Close) }) + OriginField( + ui = ui, + onEdit = if (Action.EDIT_ADDRESS in page) ({ editing = true }) else null, + onLongPress = if (!ui.chrome.isSandbox) ({ onEvent(BrowserPillEvent.CopyOrigin) }) else null, + ) + if (Action.BACK_TO_APP in page) { + OutOfScopeBanner(homeHost = BrowserChrome.displayHost(ui.chrome.startUrl)) { + onEvent(BrowserPillEvent.Action(Action.BACK_TO_APP)) + } + } + NavigationCapsule(ui, onAction = { onEvent(BrowserPillEvent.Action(it)) }) + + val tiles = page.filter { it != Action.BACK_TO_APP && it != Action.EDIT_ADDRESS } + if (tiles.isNotEmpty()) { + TileGrid( + actions = tiles, + ui = ui, + textSizeOpen = textSizeOpen, + onAction = { action -> + if (action == Action.TEXT_SIZE) textSizeOpen = !textSizeOpen else onEvent(BrowserPillEvent.Action(action)) + }, + ) + } + AnimatedVisibility(visible = textSizeOpen) { + TextSizeControl(ui.textZoom) { onEvent(BrowserPillEvent.TextZoom(it)) } + } + if (privacy.isNotEmpty()) { + PrivacyCard(ui, privacy) { onEvent(BrowserPillEvent.Action(it)) } + } + if (Action.CONSOLE in developer) { + ConsoleRow(ui) { onEvent(BrowserPillEvent.Action(Action.CONSOLE)) } + } + } + } + } +} + +@Composable +private fun PillHeader( + ui: BrowserPillUi, + showClose: Boolean, + onClose: () -> Unit, +) { + Row(verticalAlignment = Alignment.CenterVertically) { + SiteMonogram(ui.title.ifBlank { ui.host }) + Spacer(Modifier.width(12.dp)) + Text( + ui.title.ifBlank { ui.host }, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + if (showClose) { + IconButton(onClick = onClose) { + Icon(MaterialSymbols.Close, contentDescription = stringRes(Res.string.browser_pill_close), tint = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + } +} + +/** Chrome's out-of-scope bar, inside the menu: you're on another site now; one tap goes home. */ +@Composable +private fun OutOfScopeBanner( + homeHost: String, + onBack: () -> Unit, +) { + Row( + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.secondaryContainer) + .padding(start = 14.dp, end = 4.dp, top = 2.dp, bottom = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(MaterialSymbols.Info, contentDescription = null, modifier = Modifier.size(18.dp), tint = MaterialTheme.colorScheme.onSecondaryContainer) + Spacer(Modifier.width(10.dp)) + Text( + stringRes(Res.string.browser_pill_left_site, homeHost), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer, + modifier = Modifier.weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + TextButton(onClick = onBack) { Text(stringRes(Res.string.browser_pill_back_to_app)) } + } +} + +/** Page actions as tiles, four per row; toggles (desktop site, text size) fill while on. */ +@Composable +private fun TileGrid( + actions: List<Action>, + ui: BrowserPillUi, + textSizeOpen: Boolean, + onAction: (Action) -> Unit, +) { + val columns = 4 + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + actions.chunked(columns).forEach { rowActions -> + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + rowActions.forEach { action -> + ActionTile( + symbol = pillSymbolFor(action) ?: MaterialSymbols.Info, + label = stringRes(pillTileLabelFor(action)), + description = stringRes(pillLabelFor(action)), + onClick = { onAction(action) }, + selected = + when (action) { + Action.DESKTOP_SITE -> ui.desktopSite + Action.TEXT_SIZE -> textSizeOpen || ui.textZoom != BrowserChrome.DEFAULT_TEXT_ZOOM + else -> null + }, + modifier = Modifier.weight(1f), + ) + } + // Keep the last row's tiles the same width as the others. + repeat(columns - rowActions.size) { Spacer(Modifier.weight(1f)) } + } + } + } +} + +/** Text size: a stepped slider between a small and a large "A", the value, and a way back to 100%. */ +@Composable +fun TextSizeControl( + percent: Int, + onChange: (Int) -> Unit, +) { + val steps = BrowserChrome.TEXT_ZOOM_STEPS + val index = steps.indexOfFirst { it >= percent }.let { if (it < 0) steps.lastIndex else it } + Surface(shape = PillDefaults.CardShape, color = MaterialTheme.colorScheme.surfaceContainer) { + Column(Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + val smaller = stringRes(Res.string.browser_pill_text_smaller) + IconButton( + onClick = { onChange(BrowserChrome.stepTextZoom(percent, larger = false)) }, + modifier = Modifier.semantics { contentDescription = smaller }, + ) { + Text("A", fontSize = 14.sp, fontWeight = FontWeight.Medium, modifier = Modifier.padding(top = 4.dp)) + } + Slider( + value = index.toFloat(), + onValueChange = { onChange(steps[it.toInt().coerceIn(0, steps.lastIndex)]) }, + valueRange = 0f..steps.lastIndex.toFloat(), + steps = steps.size - 2, + modifier = Modifier.weight(1f), + ) + val larger = stringRes(Res.string.browser_pill_text_larger) + IconButton( + onClick = { onChange(BrowserChrome.stepTextZoom(percent, larger = true)) }, + modifier = Modifier.semantics { contentDescription = larger }, + ) { + Text("A", fontSize = 22.sp, fontWeight = FontWeight.Medium) + } + } + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + stringRes(Res.string.browser_pill_text_value, percent), + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.weight(1f).padding(start = 12.dp), + ) + TextButton(onClick = { onChange(BrowserChrome.DEFAULT_TEXT_ZOOM) }, enabled = percent != BrowserChrome.DEFAULT_TEXT_ZOOM) { + Text(stringRes(Res.string.browser_pill_text_reset)) + } + } + } + } +} + +/** + * Privacy: Tor with what it means for the site, site settings with a summary of what the site may use, + * and, for sandboxed apps, what they can access. + */ +@Composable +private fun PrivacyCard( + ui: BrowserPillUi, + actions: List<Action>, + onAction: (Action) -> Unit, +) { + GroupCard(heading = stringRes(Res.string.browser_pill_privacy)) { + actions.forEachIndexed { index, action -> + if (index > 0) GroupDivider() + when (action) { + Action.TOR -> { + val on = ui.chrome.torOn == true + GroupRow( + icon = { PillActionIcon(Action.TOR, tint = if (on) MaterialTheme.colorScheme.onTertiaryContainer else MaterialTheme.colorScheme.onSurfaceVariant, size = 22.dp) }, + iconContainer = if (on) MaterialTheme.colorScheme.tertiaryContainer else MaterialTheme.colorScheme.surfaceContainerHighest, + title = stringRes(pillLabelFor(Action.TOR)), + supporting = stringRes(if (on) Res.string.browser_pill_tor_on else Res.string.browser_pill_tor_off), + onClick = { onAction(Action.TOR) }, + ) { Switch(checked = on, onCheckedChange = { onAction(Action.TOR) }) } + } + Action.SITE_SETTINGS -> + GroupRow( + icon = { PillActionIcon(Action.SITE_SETTINGS, tint = MaterialTheme.colorScheme.onSurfaceVariant, size = 22.dp) }, + iconContainer = MaterialTheme.colorScheme.surfaceContainerHighest, + title = stringRes(pillLabelFor(Action.SITE_SETTINGS)), + supporting = permissionSummary(ui.sitePermissions), + onClick = { onAction(Action.SITE_SETTINGS) }, + ) { Icon(MaterialSymbols.ChevronRight, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant) } + Action.ACCESS_INFO -> + GroupRow( + icon = { PillActionIcon(Action.ACCESS_INFO, tint = MaterialTheme.colorScheme.onPrimaryContainer, size = 22.dp, filled = true) }, + iconContainer = MaterialTheme.colorScheme.primaryContainer, + title = stringRes(pillLabelFor(Action.ACCESS_INFO)), + supporting = stringRes(Res.string.browser_pill_access_desc), + onClick = { onAction(Action.ACCESS_INFO) }, + ) { Icon(MaterialSymbols.ChevronRight, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant) } + else -> Unit + } + } + } +} + +@Composable +private fun permissionSummary(decisions: Map<BrowserSitePermission, BrowserSitePermission.Decision>): String { + val answered = BrowserSitePermission.entries.mapNotNull { permission -> decisions[permission]?.takeIf { it != BrowserSitePermission.Decision.ASK }?.let { permission to it } } + if (answered.isEmpty()) return stringRes(Res.string.browser_pill_site_settings_none) + return answered + .map { (permission, decision) -> + stringRes( + Res.string.browser_pill_permission_state, + stringRes(permissionLabel(permission)), + stringRes(if (decision == BrowserSitePermission.Decision.ALLOW) Res.string.browser_pill_decision_allowed else Res.string.browser_pill_decision_blocked), + ) + }.joinToString(" · ") +} + +/** The developer console: one row, with an error count badge and its on/off switch. */ +@Composable +private fun ConsoleRow( + ui: BrowserPillUi, + onToggle: () -> Unit, +) { + Surface(onClick = onToggle, shape = PillDefaults.CardShape, color = MaterialTheme.colorScheme.surfaceContainer) { + Row(Modifier.padding(start = 16.dp, end = 12.dp).heightIn(min = 56.dp), verticalAlignment = Alignment.CenterVertically) { + PillActionIcon(Action.CONSOLE, tint = MaterialTheme.colorScheme.onSurfaceVariant, size = 22.dp) + Spacer(Modifier.width(16.dp)) + Text(stringRes(pillLabelFor(Action.CONSOLE)), style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f)) + if (ui.consoleErrors > 0) { + CountBadge(ui.consoleErrors) + Spacer(Modifier.width(12.dp)) + } + Switch(checked = ui.consoleShowing, onCheckedChange = { onToggle() }) + } + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillModel.kt new file mode 100644 index 0000000000..b9c78e7e1f --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillModel.kt @@ -0,0 +1,90 @@ +/* + * 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.commons.browser.ui.pill + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission + +/** + * Everything the redesigned browser pill draws, as plain immutable data. [chrome] decides *which* controls + * exist (the same [BrowserChrome] layout both browsers already share); the rest is what they show. + * + * The pill composables are stateless: a host (embedded tab, full-screen window, desktop) builds this from + * its live page and handles [BrowserPillEvent]s. + */ +@Immutable +data class BrowserPillUi( + val title: String, + val chrome: BrowserChrome.State, + val isFavorite: Boolean = false, + val desktopSite: Boolean = false, + val textZoom: Int = BrowserChrome.DEFAULT_TEXT_ZOOM, + /** 0..1 while the main frame loads; null when idle. */ + val loadProgress: Float? = null, + val consoleShowing: Boolean = false, + val consoleErrors: Int = 0, + /** Answers this site already has (camera / mic / location), for the site-settings summary. */ + val sitePermissions: Map<BrowserSitePermission, BrowserSitePermission.Decision> = emptyMap(), +) { + val security: BrowserChrome.Security get() = BrowserChrome.security(chrome) + val host: String get() = BrowserChrome.displayHost(chrome.url) +} + +/** What the user did in the pill. Hosts map these onto the WebView / broker. */ +sealed interface BrowserPillEvent { + /** A navigation-capsule button, a tile, or a privacy/developer row. */ + data class Action( + val action: BrowserChrome.Action, + ) : BrowserPillEvent + + data class TextZoom( + val percent: Int, + ) : BrowserPillEvent + + data class Navigate( + val input: String, + ) : BrowserPillEvent + + /** The origin field was long-pressed (copy link). */ + data object CopyOrigin : BrowserPillEvent + + data object Close : BrowserPillEvent +} + +/** One omnibox suggestion for the address editor. */ +@Immutable +data class AddressSuggestion( + val title: String, + val url: String, + val isFavorite: Boolean = false, +) + +/** One console line. */ +@Immutable +data class ConsoleLine( + val level: Level, + val message: String, + val source: String = "", + val line: Int = 0, +) { + enum class Level { LOG, INFO, WARNING, ERROR, DEBUG } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillPreviews.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillPreviews.kt new file mode 100644 index 0000000000..fce006a7e9 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillPreviews.kt @@ -0,0 +1,240 @@ +/* + * 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.commons.browser.ui.pill + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission +import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission.Decision +import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow + +/** Sample states for the pill prototypes, shared by the previews and the offscreen render test. */ +object BrowserPillSamples { + val primal = + BrowserPillUi( + title = "Primal", + chrome = + BrowserChrome.State( + surface = BrowserChrome.Surface.WEB, + presentation = BrowserChrome.Presentation.FULL_SCREEN, + url = "https://primal.net/home", + startUrl = "https://primal.net/", + canGoBack = true, + torOn = false, + ), + isFavorite = true, + consoleErrors = 3, + sitePermissions = mapOf(BrowserSitePermission.CAMERA to Decision.ALLOW, BrowserSitePermission.LOCATION to Decision.BLOCK), + ) + + val overTorOutOfScope = + BrowserPillUi( + title = "Sign in – Accounts", + chrome = + BrowserChrome.State( + surface = BrowserChrome.Surface.WEB, + presentation = BrowserChrome.Presentation.EMBEDDED, + url = "https://accounts.example.com/login", + startUrl = "https://snort.social/", + canGoBack = true, + canGoForward = true, + isLoading = true, + torOn = true, + ), + loadProgress = 0.62f, + textZoom = 115, + desktopSite = true, + ) + + val insecure = + BrowserPillUi( + title = "Old Forum", + chrome = BrowserChrome.State(BrowserChrome.Surface.WEB, BrowserChrome.Presentation.EMBEDDED, "http://oldforum.example.org/", "http://oldforum.example.org/"), + loadProgress = 0.3f, + ) + + val napplet = + BrowserPillUi( + title = "Zap Poll", + chrome = + BrowserChrome.State( + surface = BrowserChrome.Surface.NAPPLET, + presentation = BrowserChrome.Presentation.FULL_SCREEN, + url = "", + startUrl = "", + canFavorite = true, + hasAccessInfo = true, + torOn = true, + ), + ) + + val suggestions = + listOf( + AddressSuggestion("Primal", "https://primal.net/", isFavorite = true), + AddressSuggestion("Snort", "https://snort.social/"), + AddressSuggestion("Habla — long-form Nostr", "https://habla.news/"), + AddressSuggestion("", "https://nostr.band/search?q=amethyst"), + ) + + val console = + listOf( + ConsoleLine(ConsoleLine.Level.INFO, "Connected to wss://relay.damus.io", "app.js", 112), + ConsoleLine(ConsoleLine.Level.WARNING, "window.nostr.getRelays is deprecated", "nostr.js", 40), + ConsoleLine(ConsoleLine.Level.LOG, "feed: 42 events in 180ms", "feed.js", 9), + ConsoleLine(ConsoleLine.Level.ERROR, "Uncaught TypeError: Cannot read properties of undefined (reading 'pubkey')", "https://primal.net/assets/index-4f2a.js", 2211), + ConsoleLine(ConsoleLine.Level.ERROR, "Failed to load (-2): net::ERR_NAME_NOT_RESOLVED", "https://cdn.example.com/x.png", 0), + ConsoleLine(ConsoleLine.Level.DEBUG, "cache hit: profile 7a1c…", "cache.js", 77), + ) + + val certificate = CertificateInfo(issuedTo = "primal.net", issuedBy = "Let's Encrypt", validUntil = "Dec 14, 2026") +} + +@Composable +private fun PreviewFrame(content: @Composable () -> Unit) { + ThemeComparisonRow { + Box(Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surfaceDim).padding(bottom = 12.dp)) { content() } + } +} + +@Preview(widthDp = 820, heightDp = 1000) +@Composable +fun BrowserPillExpandedPreview() { + PreviewFrame { BrowserPill(BrowserPillSamples.primal, expanded = true, onExpandedChange = {}, onEvent = {}, showClose = true) } +} + +@Preview(widthDp = 820, heightDp = 1100) +@Composable +fun BrowserPillTorOutOfScopePreview() { + PreviewFrame { BrowserPill(BrowserPillSamples.overTorOutOfScope, expanded = true, onExpandedChange = {}, onEvent = {}, initiallyTextSizeOpen = true) } +} + +@Preview(widthDp = 820, heightDp = 700) +@Composable +fun BrowserPillAddressEditorPreview() { + PreviewFrame { + BrowserPill( + BrowserPillSamples.primal, + expanded = true, + onExpandedChange = {}, + onEvent = {}, + suggestions = BrowserPillSamples.suggestions, + clipboardUrl = "https://njump.me/npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z", + initiallyEditing = true, + ) + } +} + +@Preview(widthDp = 820, heightDp = 800) +@Composable +fun BrowserPillNappletPreview() { + PreviewFrame { BrowserPill(BrowserPillSamples.napplet, expanded = true, onExpandedChange = {}, onEvent = {}, showClose = true) } +} + +@Preview(widthDp = 820, heightDp = 160) +@Composable +fun PillHandlesPreview() { + ThemeComparisonRow { + Row(Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surfaceDim).padding(vertical = 16.dp), horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.Top) { + PillHandle(BrowserPillSamples.primal.copy(consoleErrors = 0), expanded = false, onExpandedChange = {}) + PillHandle(BrowserPillSamples.insecure, expanded = false, onExpandedChange = {}) + PillHandle(BrowserPillSamples.overTorOutOfScope.copy(consoleErrors = 2), expanded = false, onExpandedChange = {}) + } + } +} + +@Preview(widthDp = 820, heightDp = 220) +@Composable +fun FindInPagePreview() { + ThemeComparisonRow { + Column(Modifier.background(MaterialTheme.colorScheme.surfaceDim)) { + FindInPagePill(query = "zap", onQueryChange = {}, active = 2, total = 12, onNext = {}, onClose = {}, autoFocus = false) + FindInPagePill(query = "lightning address", onQueryChange = {}, active = 0, total = 0, onNext = {}, onClose = {}, autoFocus = false) + } + } +} + +@Preview(widthDp = 820, heightDp = 380) +@Composable +fun ConsoleSheetPreview() { + ThemeComparisonRow { ConsoleSheet(BrowserPillSamples.console, onCopy = {}, onClear = {}, onCopyLine = {}) } +} + +@Preview(widthDp = 820, heightDp = 700) +@Composable +fun PermissionPromptPreview() { + PreviewFrame { + Box(Modifier.padding(16.dp)) { + PermissionPromptCard("meet.example.com", BrowserChrome.Security.TOR, setOf(BrowserSitePermission.CAMERA, BrowserSitePermission.MICROPHONE), onAllow = {}, onAllowOnce = {}, onDeny = {}) + } + } +} + +@Preview(widthDp = 820, heightDp = 560) +@Composable +fun PageDialogPreview() { + PreviewFrame { + Box(Modifier.padding(16.dp)) { + PageDialogCard( + type = PageDialogType.PROMPT, + host = "snort.social", + security = BrowserChrome.Security.HTTPS, + message = "Name this relay set", + defaultValue = "Friends", + offerBlock = true, + onResult = { _, _, _ -> }, + autoFocus = false, + ) + } + } +} + +@Preview(widthDp = 820, heightDp = 420) +@Composable +fun LeaveSiteDialogPreview() { + PreviewFrame { + Box(Modifier.padding(16.dp)) { + PageDialogCard(PageDialogType.BEFORE_UNLOAD, "habla.news", BrowserChrome.Security.HTTPS, message = "", onResult = { _, _, _ -> }) + } + } +} + +@Preview(widthDp = 820, heightDp = 1250) +@Composable +fun PageInfoPreview() { + PreviewFrame { + Box(Modifier.padding(16.dp).width(380.dp)) { + PageInfoSheet(BrowserPillSamples.primal, BrowserPillSamples.certificate, onPermissionChange = { _, _ -> }, onClearSiteData = {}, initiallyConfirmingClear = true) + } + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/ConsoleSheet.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/ConsoleSheet.kt new file mode 100644 index 0000000000..28de782946 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/ConsoleSheet.kt @@ -0,0 +1,231 @@ +/* + * 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.commons.browser.ui.pill + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.browser_pill_console +import com.vitorpamplona.amethyst.commons.resources.browser_pill_console_all +import com.vitorpamplona.amethyst.commons.resources.browser_pill_console_clear +import com.vitorpamplona.amethyst.commons.resources.browser_pill_console_copy +import com.vitorpamplona.amethyst.commons.resources.browser_pill_console_empty +import com.vitorpamplona.amethyst.commons.resources.browser_pill_console_errors +import com.vitorpamplona.amethyst.commons.resources.browser_pill_console_warnings +import com.vitorpamplona.amethyst.commons.ui.stringRes + +/** Which console lines are shown. */ +enum class ConsoleFilter { ALL, ERRORS, WARNINGS } + +/** + * The developer console as a bottom sheet: a grab handle, the title with All / Errors / Warnings filter + * chips (with counts), Copy and Clear, then the log — one row per line with a level-coloured stripe, the + * message in monospace and `file:line` muted. Long-press a row to copy it. + */ +@Composable +fun ConsoleSheet( + lines: List<ConsoleLine>, + onCopy: (List<ConsoleLine>) -> Unit, + onClear: () -> Unit, + onCopyLine: (ConsoleLine) -> Unit, + modifier: Modifier = Modifier, + maxHeight: Dp = 320.dp, + initialFilter: ConsoleFilter = ConsoleFilter.ALL, +) { + var filter by remember { mutableStateOf(initialFilter) } + val errors = lines.count { it.level == ConsoleLine.Level.ERROR } + val warnings = lines.count { it.level == ConsoleLine.Level.WARNING } + val shown = + when (filter) { + ConsoleFilter.ALL -> lines + ConsoleFilter.ERRORS -> lines.filter { it.level == ConsoleLine.Level.ERROR } + ConsoleFilter.WARNINGS -> lines.filter { it.level == ConsoleLine.Level.WARNING } + } + + Surface( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp), + color = MaterialTheme.colorScheme.surfaceContainerLow, + shadowElevation = 8.dp, + border = PillDefaults.hairline(), + ) { + Column(Modifier.heightIn(max = maxHeight)) { + Box(Modifier.fillMaxWidth().padding(top = 10.dp), contentAlignment = Alignment.Center) { + Box( + Modifier + .width(32.dp) + .height(4.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)), + ) + } + Row(Modifier.padding(start = 16.dp, end = 4.dp, top = 6.dp), verticalAlignment = Alignment.CenterVertically) { + Text(stringRes(Res.string.browser_pill_console), style = MaterialTheme.typography.titleMedium, modifier = Modifier.weight(1f)) + IconButton(onClick = { onCopy(shown) }, enabled = shown.isNotEmpty()) { + Icon(MaterialSymbols.ContentCopy, contentDescription = stringRes(Res.string.browser_pill_console_copy), modifier = Modifier.size(20.dp)) + } + IconButton(onClick = onClear, enabled = lines.isNotEmpty()) { + Icon(MaterialSymbols.Delete, contentDescription = stringRes(Res.string.browser_pill_console_clear), modifier = Modifier.size(20.dp)) + } + } + Row( + Modifier.horizontalScroll(rememberScrollState()).padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + ConsoleChip(stringRes(Res.string.browser_pill_console_all), lines.size, filter == ConsoleFilter.ALL) { filter = ConsoleFilter.ALL } + ConsoleChip(stringRes(Res.string.browser_pill_console_errors), errors, filter == ConsoleFilter.ERRORS, MaterialTheme.colorScheme.error) { filter = ConsoleFilter.ERRORS } + ConsoleChip(stringRes(Res.string.browser_pill_console_warnings), warnings, filter == ConsoleFilter.WARNINGS, warningColor()) { filter = ConsoleFilter.WARNINGS } + } + if (shown.isEmpty()) { + Text( + stringRes(Res.string.browser_pill_console_empty), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 24.dp), + ) + } else { + LazyColumn(Modifier.padding(top = 6.dp, bottom = 8.dp)) { + items(shown) { line -> ConsoleRowItem(line) { onCopyLine(line) } } + } + } + } + } +} + +@Composable +private fun ConsoleChip( + label: String, + count: Int, + selected: Boolean, + dot: Color? = null, + onClick: () -> Unit, +) { + FilterChip( + selected = selected, + onClick = onClick, + label = { Text("$label $count") }, + leadingIcon = + dot?.let { color -> + { Box(Modifier.size(8.dp).clip(CircleShape).background(color)) } + }, + colors = FilterChipDefaults.filterChipColors(selectedContainerColor = MaterialTheme.colorScheme.secondaryContainer), + ) +} + +@Composable +private fun warningColor(): Color = if (MaterialTheme.colorScheme.surface.luminanceBelowHalf()) Color(0xFFFFB74D) else Color(0xFFB45309) + +private fun Color.luminanceBelowHalf(): Boolean = (0.299f * red + 0.587f * green + 0.114f * blue) < 0.5f + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun ConsoleRowItem( + line: ConsoleLine, + onLongPress: () -> Unit, +) { + val color = + when (line.level) { + ConsoleLine.Level.ERROR -> MaterialTheme.colorScheme.error + ConsoleLine.Level.WARNING -> warningColor() + ConsoleLine.Level.DEBUG -> MaterialTheme.colorScheme.onSurfaceVariant + else -> MaterialTheme.colorScheme.onSurface + } + val background = + when (line.level) { + ConsoleLine.Level.ERROR -> MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.35f) + ConsoleLine.Level.WARNING -> warningColor().copy(alpha = 0.10f) + else -> Color.Transparent + } + Row( + Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) + .background(background) + .combinedClickable(onClick = {}, onLongClick = onLongPress) + .padding(end = 16.dp), + verticalAlignment = Alignment.Top, + ) { + Box(Modifier.width(3.dp).fillMaxHeight().background(if (line.level == ConsoleLine.Level.LOG) Color.Transparent else color)) + Spacer(Modifier.width(13.dp)) + Text( + line.message, + color = color, + fontFamily = FontFamily.Monospace, + fontSize = 12.sp, + lineHeight = 16.sp, + modifier = Modifier.weight(1f).padding(vertical = 5.dp), + ) + if (line.source.isNotBlank()) { + Spacer(Modifier.width(8.dp)) + Text( + line.source.substringAfterLast('/') + ":" + line.line, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontFamily = FontFamily.Monospace, + fontSize = 11.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(vertical = 5.dp).width(96.dp), + ) + } + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/FindInPagePill.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/FindInPagePill.kt new file mode 100644 index 0000000000..c38bdfbc2c --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/FindInPagePill.kt @@ -0,0 +1,135 @@ +/* + * 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.commons.browser.ui.pill + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.browser_pill_find_close +import com.vitorpamplona.amethyst.commons.resources.browser_pill_find_count +import com.vitorpamplona.amethyst.commons.resources.browser_pill_find_hint +import com.vitorpamplona.amethyst.commons.resources.browser_pill_find_next +import com.vitorpamplona.amethyst.commons.resources.browser_pill_find_none +import com.vitorpamplona.amethyst.commons.resources.browser_pill_find_previous +import com.vitorpamplona.amethyst.commons.ui.stringRes + +/** + * Find in page as a floating capsule above the bottom edge: search glyph, the query, a match chip + * ("3 / 12", or "No matches" in the error tone), previous / next, and close. IME Search jumps to the next + * match. [active] is 0-based; [total] null means no search has run yet. + */ +@Composable +fun FindInPagePill( + query: String, + onQueryChange: (String) -> Unit, + active: Int, + total: Int?, + onNext: (forward: Boolean) -> Unit, + onClose: () -> Unit, + modifier: Modifier = Modifier, + autoFocus: Boolean = true, +) { + val focus = remember { FocusRequester() } + if (autoFocus) LaunchedEffect(Unit) { runCatching { focus.requestFocus() } } + val noMatches = query.isNotEmpty() && total == 0 + + Surface( + modifier = modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 12.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.surfaceContainerHigh, + shadowElevation = 8.dp, + border = PillDefaults.hairline(), + ) { + Row(Modifier.height(56.dp).padding(start = 18.dp, end = 4.dp), verticalAlignment = Alignment.CenterVertically) { + Icon(MaterialSymbols.Search, contentDescription = null, modifier = Modifier.size(20.dp), tint = if (noMatches) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant) + Spacer(Modifier.width(12.dp)) + Box(Modifier.weight(1f), contentAlignment = Alignment.CenterStart) { + if (query.isEmpty()) { + Text(stringRes(Res.string.browser_pill_find_hint), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + BasicTextField( + value = query, + onValueChange = onQueryChange, + singleLine = true, + textStyle = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface), + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions(onSearch = { onNext(true) }), + modifier = Modifier.fillMaxWidth().focusRequester(focus), + ) + } + if (query.isNotEmpty() && total != null) { + Spacer(Modifier.width(8.dp)) + Box( + Modifier + .clip(CircleShape) + .background(if (noMatches) MaterialTheme.colorScheme.errorContainer else MaterialTheme.colorScheme.secondaryContainer) + .padding(horizontal = 10.dp, vertical = 4.dp), + ) { + Text( + if (noMatches) stringRes(Res.string.browser_pill_find_none) else stringRes(Res.string.browser_pill_find_count, active + 1, total), + style = MaterialTheme.typography.labelMedium, + color = if (noMatches) MaterialTheme.colorScheme.onErrorContainer else MaterialTheme.colorScheme.onSecondaryContainer, + ) + } + } + IconButton(onClick = { onNext(false) }, enabled = (total ?: 0) > 0) { + Icon(MaterialSymbols.KeyboardArrowUp, contentDescription = stringRes(Res.string.browser_pill_find_previous)) + } + IconButton(onClick = { onNext(true) }, enabled = (total ?: 0) > 0) { + Icon(MaterialSymbols.KeyboardArrowDown, contentDescription = stringRes(Res.string.browser_pill_find_next)) + } + VerticalDivider(Modifier.height(24.dp), color = MaterialTheme.colorScheme.outlineVariant) + IconButton(onClick = onClose) { + Icon(MaterialSymbols.Close, contentDescription = stringRes(Res.string.browser_pill_find_close)) + } + } + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/PageSheets.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/PageSheets.kt new file mode 100644 index 0000000000..8c28a74bc7 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/PageSheets.kt @@ -0,0 +1,476 @@ +/* + * 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.commons.browser.ui.pill + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission +import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission.Decision +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.browser_pill_cancel +import com.vitorpamplona.amethyst.commons.resources.browser_pill_clear +import com.vitorpamplona.amethyst.commons.resources.browser_pill_decision_allow +import com.vitorpamplona.amethyst.commons.resources.browser_pill_decision_ask +import com.vitorpamplona.amethyst.commons.resources.browser_pill_decision_block +import com.vitorpamplona.amethyst.commons.resources.browser_pill_dialog_answer +import com.vitorpamplona.amethyst.commons.resources.browser_pill_dialog_block +import com.vitorpamplona.amethyst.commons.resources.browser_pill_dialog_generic +import com.vitorpamplona.amethyst.commons.resources.browser_pill_dialog_leave +import com.vitorpamplona.amethyst.commons.resources.browser_pill_dialog_leave_message +import com.vitorpamplona.amethyst.commons.resources.browser_pill_dialog_leave_title +import com.vitorpamplona.amethyst.commons.resources.browser_pill_dialog_says +import com.vitorpamplona.amethyst.commons.resources.browser_pill_info_certificate +import com.vitorpamplona.amethyst.commons.resources.browser_pill_info_certificate_desc +import com.vitorpamplona.amethyst.commons.resources.browser_pill_info_clear +import com.vitorpamplona.amethyst.commons.resources.browser_pill_info_clear_confirm +import com.vitorpamplona.amethyst.commons.resources.browser_pill_info_connection +import com.vitorpamplona.amethyst.commons.resources.browser_pill_info_http +import com.vitorpamplona.amethyst.commons.resources.browser_pill_info_http_desc +import com.vitorpamplona.amethyst.commons.resources.browser_pill_info_https +import com.vitorpamplona.amethyst.commons.resources.browser_pill_info_https_desc +import com.vitorpamplona.amethyst.commons.resources.browser_pill_info_open +import com.vitorpamplona.amethyst.commons.resources.browser_pill_info_permissions +import com.vitorpamplona.amethyst.commons.resources.browser_pill_info_site_data +import com.vitorpamplona.amethyst.commons.resources.browser_pill_info_site_data_desc +import com.vitorpamplona.amethyst.commons.resources.browser_pill_info_tor +import com.vitorpamplona.amethyst.commons.resources.browser_pill_ok +import com.vitorpamplona.amethyst.commons.resources.browser_pill_perm_allow +import com.vitorpamplona.amethyst.commons.resources.browser_pill_perm_camera_desc +import com.vitorpamplona.amethyst.commons.resources.browser_pill_perm_deny +import com.vitorpamplona.amethyst.commons.resources.browser_pill_perm_location_desc +import com.vitorpamplona.amethyst.commons.resources.browser_pill_perm_microphone_desc +import com.vitorpamplona.amethyst.commons.resources.browser_pill_perm_once +import com.vitorpamplona.amethyst.commons.resources.browser_pill_perm_title +import com.vitorpamplona.amethyst.commons.resources.browser_pill_perm_tor_note +import com.vitorpamplona.amethyst.commons.resources.browser_pill_tor_off +import com.vitorpamplona.amethyst.commons.resources.browser_pill_tor_on +import com.vitorpamplona.amethyst.commons.ui.stringRes + +/** A compact, read-only origin badge for card headers: the page can't fake it, so every card starts with it. */ +@Composable +fun OriginBadge( + host: String, + security: BrowserChrome.Security, + modifier: Modifier = Modifier, +) { + Row( + modifier + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHighest) + .padding(start = 10.dp, end = 12.dp, top = 6.dp, bottom = 6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + SecurityIcon(security, size = 16.dp) + Spacer(Modifier.width(6.dp)) + Text(host, style = MaterialTheme.typography.labelLarge, maxLines = 1, overflow = TextOverflow.Ellipsis) + } +} + +/** The card frame every page-initiated prompt shares. */ +@Composable +private fun PageCard(content: @Composable () -> Unit) { + Surface( + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + shadowElevation = 6.dp, + border = PillDefaults.hairline(), + modifier = Modifier.fillMaxWidth(), + ) { + Column(Modifier.padding(24.dp)) { content() } + } +} + +/** + * A site asking for camera / microphone / location: the origin badge, a tinted circle per thing asked with + * what it means, a note when calls over Tor could still expose the IP, and three plain-worded answers — + * remembered allow, one-time allow, remembered refusal. + */ +@Composable +fun PermissionPromptCard( + host: String, + security: BrowserChrome.Security, + permissions: Set<BrowserSitePermission>, + onAllow: () -> Unit, + onAllowOnce: () -> Unit, + onDeny: () -> Unit, +) { + PageCard { + OriginBadge(host, security) + Spacer(Modifier.height(20.dp)) + Text( + stringRes(Res.string.browser_pill_perm_title), + style = MaterialTheme.typography.titleLarge, + ) + Spacer(Modifier.height(16.dp)) + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + permissions.sortedBy { it.ordinal }.forEach { permission -> + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + Modifier.size(44.dp).clip(CircleShape).background(MaterialTheme.colorScheme.primaryContainer), + contentAlignment = Alignment.Center, + ) { + Icon(permissionSymbol(permission), contentDescription = null, tint = MaterialTheme.colorScheme.onPrimaryContainer, filled = true) + } + Spacer(Modifier.width(14.dp)) + Column { + Text(stringRes(permissionLabel(permission)), style = MaterialTheme.typography.titleSmall) + Text( + stringRes( + when (permission) { + BrowserSitePermission.CAMERA -> Res.string.browser_pill_perm_camera_desc + BrowserSitePermission.MICROPHONE -> Res.string.browser_pill_perm_microphone_desc + BrowserSitePermission.LOCATION -> Res.string.browser_pill_perm_location_desc + }, + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } + // WebRTC can reach out around the SOCKS proxy; say so where it matters, without blocking the call. + if (security == BrowserChrome.Security.TOR && (BrowserSitePermission.CAMERA in permissions || BrowserSitePermission.MICROPHONE in permissions)) { + Spacer(Modifier.height(16.dp)) + Row( + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.6f)) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + PillActionIcon(BrowserChrome.Action.TOR, tint = MaterialTheme.colorScheme.onTertiaryContainer, size = 18.dp) + Spacer(Modifier.width(10.dp)) + Text(stringRes(Res.string.browser_pill_perm_tor_note), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onTertiaryContainer) + } + } + Spacer(Modifier.height(24.dp)) + Button(onClick = onAllow, modifier = Modifier.fillMaxWidth().height(48.dp)) { Text(stringRes(Res.string.browser_pill_perm_allow)) } + Spacer(Modifier.height(8.dp)) + FilledTonalButton(onClick = onAllowOnce, modifier = Modifier.fillMaxWidth().height(48.dp)) { Text(stringRes(Res.string.browser_pill_perm_once)) } + Spacer(Modifier.height(4.dp)) + TextButton(onClick = onDeny, modifier = Modifier.fillMaxWidth().height(48.dp)) { Text(stringRes(Res.string.browser_pill_perm_deny)) } + } +} + +/** The kinds of page dialog. */ +enum class PageDialogType { ALERT, CONFIRM, PROMPT, BEFORE_UNLOAD } + +/** + * A page's alert / confirm / prompt / beforeunload. The origin badge heads it (so it can't pass for + * Amethyst UI); prompt gets a labelled field with clear and IME Done; repeat dialogs offer a checkbox to + * block the rest, as Chrome does. + */ +@Composable +fun PageDialogCard( + type: PageDialogType, + host: String?, + security: BrowserChrome.Security, + message: String, + defaultValue: String = "", + offerBlock: Boolean = false, + onResult: (confirmed: Boolean, text: String?, block: Boolean) -> Unit, + autoFocus: Boolean = true, +) { + var text by remember { mutableStateOf(defaultValue) } + var block by remember { mutableStateOf(false) } + val leave = type == PageDialogType.BEFORE_UNLOAD + val focus = remember { FocusRequester() } + if (type == PageDialogType.PROMPT && autoFocus) LaunchedEffect(Unit) { runCatching { focus.requestFocus() } } + + PageCard { + if (host != null) { + OriginBadge(host, security) + Spacer(Modifier.height(16.dp)) + } + Text( + when { + leave -> stringRes(Res.string.browser_pill_dialog_leave_title) + host != null -> stringRes(Res.string.browser_pill_dialog_says, host) + else -> stringRes(Res.string.browser_pill_dialog_generic) + }, + style = MaterialTheme.typography.headlineSmall, + ) + Spacer(Modifier.height(12.dp)) + Column(Modifier.heightIn(max = 240.dp).verticalScroll(rememberScrollState())) { + Text( + if (leave) stringRes(Res.string.browser_pill_dialog_leave_message) else message, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + if (type == PageDialogType.PROMPT) { + Spacer(Modifier.height(16.dp)) + OutlinedTextField( + value = text, + onValueChange = { text = it }, + label = { Text(stringRes(Res.string.browser_pill_dialog_answer)) }, + singleLine = true, + trailingIcon = { + if (text.isNotEmpty()) { + IconButton(onClick = { text = "" }) { Icon(MaterialSymbols.Cancel, contentDescription = stringRes(Res.string.browser_pill_clear)) } + } + }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { onResult(true, text, block) }), + shape = RoundedCornerShape(16.dp), + modifier = Modifier.fillMaxWidth().focusRequester(focus), + ) + } + if (offerBlock) { + Spacer(Modifier.height(8.dp)) + Row( + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .clickable { block = !block } + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox(checked = block, onCheckedChange = { block = it }) + Text(stringRes(Res.string.browser_pill_dialog_block), style = MaterialTheme.typography.bodyMedium) + } + } + Spacer(Modifier.height(20.dp)) + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + if (type != PageDialogType.ALERT) { + TextButton(onClick = { onResult(false, null, block) }) { Text(stringRes(Res.string.browser_pill_cancel)) } + Spacer(Modifier.width(8.dp)) + } + Button( + onClick = { onResult(true, if (type == PageDialogType.PROMPT) text else null, block) }, + colors = if (leave) ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error, contentColor = MaterialTheme.colorScheme.onError) else ButtonDefaults.buttonColors(), + ) { + Text(stringRes(if (leave) Res.string.browser_pill_dialog_leave else Res.string.browser_pill_ok)) + } + } + } +} + +/** Certificate facts for [PageInfoSheet]. */ +data class CertificateInfo( + val issuedTo: String, + val issuedBy: String, + val validUntil: String, +) + +/** + * Page info as a sheet: who the site is, how you're connected (encryption, route, certificate), what it + * may use (camera / mic / location as Ask · Allow · Block, editable in place), and its stored data with a + * clear that asks first. + */ +@Composable +fun PageInfoSheet( + ui: BrowserPillUi, + certificate: CertificateInfo?, + onPermissionChange: (BrowserSitePermission, Decision) -> Unit, + onClearSiteData: () -> Unit, + modifier: Modifier = Modifier, + initiallyConfirmingClear: Boolean = false, +) { + var confirmingClear by remember { mutableStateOf(initiallyConfirmingClear) } + val https = ui.chrome.url.startsWith("https://", ignoreCase = true) + Surface( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surface, + shadowElevation = 6.dp, + border = PillDefaults.hairline(), + ) { + Column(Modifier.padding(PillDefaults.SheetPadding), verticalArrangement = Arrangement.spacedBy(16.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + SiteMonogram(ui.title.ifBlank { ui.host }, size = 48.dp) + Spacer(Modifier.width(14.dp)) + Column { + Text(ui.host, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis) + Text(BrowserChrome.originOf(ui.chrome.url) ?: ui.chrome.url, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis) + } + } + + GroupCard(heading = stringRes(Res.string.browser_pill_info_connection)) { + GroupRow( + icon = { Icon(if (https) MaterialSymbols.Lock else MaterialSymbols.NoEncryption, contentDescription = null, tint = if (https) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onErrorContainer, filled = true) }, + iconContainer = if (https) MaterialTheme.colorScheme.surfaceContainerHighest else MaterialTheme.colorScheme.errorContainer, + title = stringRes(if (https) Res.string.browser_pill_info_https else Res.string.browser_pill_info_http), + supporting = stringRes(if (https) Res.string.browser_pill_info_https_desc else Res.string.browser_pill_info_http_desc), + supportingColor = if (https) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.error, + onClick = null, + ) + ui.chrome.torOn?.let { tor -> + GroupDivider() + GroupRow( + icon = { PillActionIcon(BrowserChrome.Action.TOR, tint = if (tor) MaterialTheme.colorScheme.onTertiaryContainer else MaterialTheme.colorScheme.onSurfaceVariant, size = 22.dp) }, + iconContainer = if (tor) MaterialTheme.colorScheme.tertiaryContainer else MaterialTheme.colorScheme.surfaceContainerHighest, + title = stringRes(if (tor) Res.string.browser_pill_info_tor else Res.string.browser_pill_info_open), + supporting = stringRes(if (tor) Res.string.browser_pill_tor_on else Res.string.browser_pill_tor_off), + onClick = null, + ) + } + if (certificate != null) { + GroupDivider() + GroupRow( + icon = { Icon(MaterialSymbols.Shield, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant) }, + iconContainer = MaterialTheme.colorScheme.surfaceContainerHighest, + title = stringRes(Res.string.browser_pill_info_certificate), + supporting = stringRes(Res.string.browser_pill_info_certificate_desc, certificate.issuedTo, certificate.issuedBy, certificate.validUntil), + onClick = null, + ) + } + } + + GroupCard(heading = stringRes(Res.string.browser_pill_info_permissions)) { + BrowserSitePermission.entries.forEachIndexed { index, permission -> + if (index > 0) GroupDivider() + PermissionDecisionRow(permission, ui.sitePermissions[permission] ?: Decision.ASK) { onPermissionChange(permission, it) } + } + } + + GroupCard(heading = stringRes(Res.string.browser_pill_info_site_data)) { + Column(Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(MaterialSymbols.Cookie, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant) + Spacer(Modifier.width(12.dp)) + Text(stringRes(Res.string.browser_pill_info_site_data_desc), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + Spacer(Modifier.height(12.dp)) + AnimatedContent(targetState = confirmingClear, label = "clear-site-data") { confirming -> + if (!confirming) { + OutlinedButton( + onClick = { confirmingClear = true }, + colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.error), + modifier = Modifier.fillMaxWidth(), + ) { + Icon(MaterialSymbols.Delete, contentDescription = null, modifier = Modifier.size(18.dp)) + Spacer(Modifier.width(8.dp)) + Text(stringRes(Res.string.browser_pill_info_clear)) + } + } else { + Column( + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.errorContainer) + .padding(12.dp), + ) { + Text(stringRes(Res.string.browser_pill_info_clear_confirm), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onErrorContainer) + Spacer(Modifier.height(8.dp)) + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + TextButton(onClick = { confirmingClear = false }) { Text(stringRes(Res.string.browser_pill_cancel), color = MaterialTheme.colorScheme.onErrorContainer) } + Spacer(Modifier.width(8.dp)) + Button( + onClick = { + confirmingClear = false + onClearSiteData() + }, + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error, contentColor = MaterialTheme.colorScheme.onError), + ) { Text(stringRes(Res.string.browser_pill_info_clear)) } + } + } + } + } + } + } + } + } +} + +/** One permission with a first-class Ask · Allow · Block choice. */ +@Composable +private fun PermissionDecisionRow( + permission: BrowserSitePermission, + decision: Decision, + onChange: (Decision) -> Unit, +) { + Column(Modifier.padding(horizontal = 12.dp, vertical = 10.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box(Modifier.size(40.dp).clip(CircleShape).background(MaterialTheme.colorScheme.surfaceContainerHighest), contentAlignment = Alignment.Center) { + Icon(permissionSymbol(permission), contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant) + } + Spacer(Modifier.width(12.dp)) + Text(stringRes(permissionLabel(permission)), style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f)) + } + Spacer(Modifier.height(8.dp)) + val options = listOf(Decision.ASK to Res.string.browser_pill_decision_ask, Decision.ALLOW to Res.string.browser_pill_decision_allow, Decision.BLOCK to Res.string.browser_pill_decision_block) + SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth().padding(start = 52.dp)) { + options.forEachIndexed { index, (option, label) -> + SegmentedButton( + selected = decision == option, + onClick = { onChange(option) }, + shape = SegmentedButtonDefaults.itemShape(index, options.size), + icon = {}, + label = { Text(stringRes(label), textAlign = TextAlign.Center, maxLines = 1) }, + ) + } + } + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/PillComponents.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/PillComponents.kt new file mode 100644 index 0000000000..1dc373369b --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/PillComponents.kt @@ -0,0 +1,348 @@ +/* + * 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.commons.browser.ui.pill + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.Action +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.browser_pill_edit_address +import com.vitorpamplona.amethyst.commons.ui.stringRes + +/** Shapes and sizes shared by every pill surface, so they read as one family. */ +object PillDefaults { + val SheetShape = RoundedCornerShape(bottomStart = 28.dp, bottomEnd = 28.dp) + val CardShape = RoundedCornerShape(20.dp) + val TileShape = RoundedCornerShape(18.dp) + val SheetPadding = 16.dp + val TileHeight = 88.dp + val FieldHeight = 48.dp + + /** The edge every floating pill surface draws, so it stays visible where shadows don't (black theme). */ + @Composable + fun hairline(): BorderStroke = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f)) +} + +/** + * A site's stand-in icon: its first letter on a tonal rounded square. Real favicons can drop in later + * (the launcher already stores them per host); the monogram keeps the header from ever being empty. + */ +@Composable +fun SiteMonogram( + label: String, + modifier: Modifier = Modifier, + size: Dp = 40.dp, +) { + val letter = label.firstOrNull { it.isLetterOrDigit() }?.uppercaseChar()?.toString() ?: "•" + Box( + modifier + .size(size) + .clip(RoundedCornerShape(size * 0.3f)) + .background(MaterialTheme.colorScheme.primaryContainer), + contentAlignment = Alignment.Center, + ) { + Text( + letter, + style = if (size >= 40.dp) MaterialTheme.typography.titleMedium else MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } +} + +/** + * The origin as a read-only field: security badge in its signal colour, host, and a trailing pencil that + * says "this is where the address lives". Tapping edits the address; long-pressing copies the link. + * Sandboxed apps get a non-editable version with no pencil. + */ +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun OriginField( + ui: BrowserPillUi, + onEdit: (() -> Unit)?, + onLongPress: (() -> Unit)?, + modifier: Modifier = Modifier, +) { + val security = ui.security + Row( + modifier + .fillMaxWidth() + .heightIn(min = PillDefaults.FieldHeight) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHighest) + .combinedClickable( + enabled = onEdit != null || onLongPress != null, + role = Role.Button, + onClick = { onEdit?.invoke() }, + onLongClick = onLongPress, + ).padding(start = 16.dp, end = 6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + SecurityIcon(security, size = 18.dp) + Spacer(Modifier.width(10.dp)) + Column(Modifier.weight(1f).padding(vertical = 6.dp)) { + Text( + if (ui.chrome.isSandbox) stringRes(securityLabel(security)) else ui.host, + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + // Only the states worth a second look get words: plain HTTP and Tor. + if (!ui.chrome.isSandbox && (security == BrowserChrome.Security.HTTP || security == BrowserChrome.Security.TOR)) { + Text( + stringRes(securityLabel(security)), + style = MaterialTheme.typography.labelSmall, + color = securityTint(security), + maxLines = 1, + ) + } + } + if (onEdit != null) { + IconButton(onClick = onEdit) { + Icon(MaterialSymbols.Edit, contentDescription = stringRes(Res.string.browser_pill_edit_address), modifier = Modifier.size(20.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant) + } + } else { + Spacer(Modifier.width(10.dp)) + } + } +} + +/** + * The navigation capsule: Chrome's back · forward · reload/stop · star · share on one rounded bar. Back + * and forward dim when unavailable; the star fills when pinned; reload turns into stop, ringed by the load. + */ +@Composable +fun NavigationCapsule( + ui: BrowserPillUi, + onAction: (Action) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier + .fillMaxWidth() + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + .padding(horizontal = 4.dp, vertical = 2.dp), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + ) { + BrowserChrome.iconRow(ui.chrome).forEach { action -> + val enabled = BrowserChrome.isEnabled(ui.chrome, action) + val pinned = action == Action.FAVORITE && ui.isFavorite + IconButton( + onClick = { onAction(action) }, + enabled = enabled, + colors = IconButtonDefaults.iconButtonColors(disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f)), + ) { + Box(contentAlignment = Alignment.Center) { + if (action == Action.STOP && ui.loadProgress != null) { + CircularProgressIndicator( + progress = { ui.loadProgress }, + modifier = Modifier.size(34.dp), + strokeWidth = 2.dp, + trackColor = Color.Transparent, + ) + } + PillActionIcon( + action = action, + tint = + when { + !enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) + pinned -> MaterialTheme.colorScheme.primary + else -> MaterialTheme.colorScheme.onSurface + }, + size = if (action == Action.STOP) 20.dp else 24.dp, + filled = pinned, + contentDescription = stringRes(pillLabelFor(action, ui.isFavorite)), + ) + } + } + } + } +} + +/** + * A page-action tile: icon over a two-line label on a rounded card. A toggle tile ([selected] non-null) + * fills with the secondary container while on, so its state reads without a switch. + */ +@Composable +fun ActionTile( + symbol: MaterialSymbol, + label: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + selected: Boolean? = null, + description: String = label, +) { + val on = selected == true + Surface( + onClick = onClick, + modifier = modifier.height(PillDefaults.TileHeight).semantics(mergeDescendants = true) { contentDescription = description }, + shape = PillDefaults.TileShape, + color = if (on) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surfaceContainer, + contentColor = if (on) MaterialTheme.colorScheme.onSecondaryContainer else MaterialTheme.colorScheme.onSurface, + ) { + Column( + // Top-aligned so every icon in a row sits on one line, whether its label takes one line or two. + Modifier.padding(start = 4.dp, end = 4.dp, top = 16.dp, bottom = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + ) { + Icon(symbol, contentDescription = null, modifier = Modifier.size(24.dp), filled = on) + Spacer(Modifier.height(6.dp)) + Text( + label, + style = MaterialTheme.typography.labelMedium, + textAlign = TextAlign.Center, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +/** A rounded group of rows (the privacy card), with an optional heading above it. */ +@Composable +fun GroupCard( + heading: String?, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit, +) { + Column(modifier.fillMaxWidth()) { + if (heading != null) { + Text( + heading, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 12.dp, bottom = 6.dp), + ) + } + Surface(shape = PillDefaults.CardShape, color = MaterialTheme.colorScheme.surfaceContainer) { + Column(Modifier.padding(vertical = 4.dp), content = content) + } + } +} + +/** A divider inset past a [GroupRow]'s leading icon. */ +@Composable +fun GroupDivider() { + HorizontalDivider(Modifier.padding(start = 64.dp, end = 16.dp), color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) +} + +/** + * One row of a [GroupCard]: a tinted icon circle, a title with a supporting line that states the + * consequence, and a trailing control (switch, chevron, badge). + */ +@Composable +fun GroupRow( + icon: @Composable () -> Unit, + iconContainer: Color, + title: String, + supporting: String?, + onClick: (() -> Unit)?, + modifier: Modifier = Modifier, + supportingColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + trailing: @Composable RowScope.() -> Unit = {}, +) { + Row( + modifier + .fillMaxWidth() + .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier) + .padding(horizontal = 12.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box(Modifier.size(40.dp).clip(CircleShape).background(iconContainer), contentAlignment = Alignment.Center) { icon() } + Spacer(Modifier.width(12.dp)) + Column(Modifier.weight(1f)) { + Text(title, style = MaterialTheme.typography.bodyLarge, maxLines = 1, overflow = TextOverflow.Ellipsis) + if (supporting != null) { + Text(supporting, style = MaterialTheme.typography.bodySmall, color = supportingColor, maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + Spacer(Modifier.width(8.dp)) + trailing() + } +} + +/** A small count badge (console errors). */ +@Composable +fun CountBadge( + count: Int, + container: Color = MaterialTheme.colorScheme.errorContainer, + content: Color = MaterialTheme.colorScheme.onErrorContainer, +) { + Box( + Modifier + .heightIn(min = 22.dp) + .clip(CircleShape) + .background(container) + .padding(horizontal = 8.dp), + contentAlignment = Alignment.Center, + ) { + Text(if (count > 99) "99+" else count.toString(), style = MaterialTheme.typography.labelMedium, color = content) + } +} + +/** Dims content that is shown but not usable. */ +fun Modifier.dimmed(enabled: Boolean): Modifier = if (enabled) this else this.alpha(0.38f) diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/PillIcons.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/PillIcons.kt new file mode 100644 index 0000000000..abcb5c8ac7 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/PillIcons.kt @@ -0,0 +1,215 @@ +/* + * 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.commons.browser.ui.pill + +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.PathParser +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.Action +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.Security +import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.browser_pill_access +import com.vitorpamplona.amethyst.commons.resources.browser_pill_add_home +import com.vitorpamplona.amethyst.commons.resources.browser_pill_back +import com.vitorpamplona.amethyst.commons.resources.browser_pill_back_to_app +import com.vitorpamplona.amethyst.commons.resources.browser_pill_console +import com.vitorpamplona.amethyst.commons.resources.browser_pill_copy +import com.vitorpamplona.amethyst.commons.resources.browser_pill_desktop +import com.vitorpamplona.amethyst.commons.resources.browser_pill_edit_address +import com.vitorpamplona.amethyst.commons.resources.browser_pill_favorite_add +import com.vitorpamplona.amethyst.commons.resources.browser_pill_favorite_remove +import com.vitorpamplona.amethyst.commons.resources.browser_pill_find +import com.vitorpamplona.amethyst.commons.resources.browser_pill_forward +import com.vitorpamplona.amethyst.commons.resources.browser_pill_full_screen +import com.vitorpamplona.amethyst.commons.resources.browser_pill_other_browser +import com.vitorpamplona.amethyst.commons.resources.browser_pill_permission_camera +import com.vitorpamplona.amethyst.commons.resources.browser_pill_permission_location +import com.vitorpamplona.amethyst.commons.resources.browser_pill_permission_microphone +import com.vitorpamplona.amethyst.commons.resources.browser_pill_reload +import com.vitorpamplona.amethyst.commons.resources.browser_pill_security_http +import com.vitorpamplona.amethyst.commons.resources.browser_pill_security_https +import com.vitorpamplona.amethyst.commons.resources.browser_pill_security_sandbox +import com.vitorpamplona.amethyst.commons.resources.browser_pill_security_tor +import com.vitorpamplona.amethyst.commons.resources.browser_pill_share +import com.vitorpamplona.amethyst.commons.resources.browser_pill_site_settings +import com.vitorpamplona.amethyst.commons.resources.browser_pill_stop +import com.vitorpamplona.amethyst.commons.resources.browser_pill_text_size +import com.vitorpamplona.amethyst.commons.resources.browser_pill_tile_copy +import com.vitorpamplona.amethyst.commons.resources.browser_pill_tile_desktop +import com.vitorpamplona.amethyst.commons.resources.browser_pill_tile_find +import com.vitorpamplona.amethyst.commons.resources.browser_pill_tile_full +import com.vitorpamplona.amethyst.commons.resources.browser_pill_tile_home +import com.vitorpamplona.amethyst.commons.resources.browser_pill_tile_other +import com.vitorpamplona.amethyst.commons.resources.browser_pill_tile_text +import com.vitorpamplona.amethyst.commons.resources.browser_pill_tor_title +import org.jetbrains.compose.resources.StringResource +import androidx.compose.material3.Icon as Material3Icon + +/** Tor's onion, as a vector (the same artwork as the Android `ic_tor` drawable). */ +val OnionIcon: ImageVector by lazy { + ImageVector + .Builder(name = "Onion", defaultWidth = 24.dp, defaultHeight = 24.dp, viewportWidth = 24f, viewportHeight = 24f) + .addPath(pathData = PathParser().parsePathString(ONION_PATH).toNodes(), fill = SolidColor(Color.Black)) + .build() +} + +private const val ONION_PATH = "M17.578,12.201c-0.76,-0.692 -1.721,-1.251 -2.704,-1.81 -0.446,-0.246 -1.81,-1.318 -1.34,-2.838l-0.851,-0.358c1.342,-2.078 3.085,-4.134 5.229,-6.056 -1.721,0.581 -3.24,1.476 -4.379,3.062 0.67,-1.407 1.765,-2.793 2.972,-4.201 -1.654,1.185 -3.084,2.525 -3.979,4.313l0.627,-2.503c-0.894,1.608 -1.52,3.24 -1.766,4.871l-1.317,-0.535 -0.223,0.178c1.162,2.078 0.559,3.174 -0.022,3.553 -1.162,0.783 -2.838,1.788 -3.688,2.659 -1.609,1.654 -2.078,3.218 -1.921,5.296 0.157,2.66 2.101,4.873 4.67,5.744 1.14,0.38 2.19,0.424 3.352,0.424 1.877,0 3.799,-0.491 5.207,-1.676a6.551,6.551 0,0 0,2.369 -5.027,6.875 6.875,0 0,0 -2.236,-5.096zM14.025,21.073c-0.09,0.402 -0.38,0.894 -0.737,1.341 0.134,-0.246 0.246,-0.492 0.313,-0.76 0.559,-1.989 0.805,-2.904 0.537,-5.095 -0.045,-0.224 -0.135,-0.938 -0.471,-1.721 -0.468,-1.185 -1.184,-2.303 -1.272,-2.548 -0.157,-0.38 -0.38,-1.989 -0.403,-3.084 0.023,0.938 0.089,2.659 0.335,3.329 0.067,0.225 0.715,1.229 1.185,2.459 0.312,0.849 0.38,1.632 0.446,1.854 0.224,1.007 -0.045,2.705 -0.401,4.313 -0.111,0.581 -0.426,1.252 -0.828,1.766 0.225,-0.313 0.402,-0.715 0.537,-1.185 0.269,-0.938 0.38,-2.145 0.356,-2.905 -0.021,-0.446 -0.222,-1.407 -0.558,-2.278 -0.201,-0.47 -0.492,-0.961 -0.692,-1.297 -0.224,-0.335 -0.224,-1.072 -0.313,-1.921 0.021,0.916 -0.068,1.385 0.156,2.033 0.134,0.379 0.625,0.916 0.759,1.43 0.201,0.693 0.402,1.453 0.381,1.922 0,0.536 -0.022,1.52 -0.269,2.593 -0.157,0.804 -0.515,1.497 -1.095,1.943 0.246,-0.312 0.38,-0.625 0.447,-0.938 0.089,-0.469 0.111,-0.916 0.156,-1.475a5.96,5.96 0,0 0,-0.111 -1.721c-0.179,-0.805 -0.469,-1.608 -0.604,-2.168 0.022,0.626 0.269,1.408 0.381,2.235 0.089,0.604 0.044,1.206 0.021,1.742 -0.021,0.627 -0.223,1.722 -0.492,2.258 -0.268,-0.112 -0.357,-0.269 -0.537,-0.491 -0.223,-0.291 -0.357,-0.604 -0.491,-0.962a5.043,5.043 0,0 1,-0.291 -0.915,3.071 3.071,0 0,1 0.559,-2.213c0.469,-0.671 0.559,-0.716 0.715,-1.497 -0.223,0.692 -0.379,0.759 -0.871,1.341 -0.559,0.647 -0.648,1.586 -0.648,2.346 0,0.313 0.134,0.671 0.246,1.007 0.134,0.356 0.268,0.714 0.447,0.982 0.134,0.223 0.313,0.379 0.469,0.491 -0.581,-0.156 -1.184,-0.379 -1.564,-0.692 -0.938,-0.805 -1.765,-2.167 -1.877,-3.375 -0.089,-0.982 0.804,-2.413 2.078,-3.128 1.073,-0.626 1.318,-1.319 1.542,-2.459 -0.313,0.983 -0.626,1.833 -1.654,2.348 -1.475,0.804 -2.235,2.1 -2.167,3.352 0.112,1.586 0.737,2.682 2.011,3.554 0.291,0.2 0.693,0.401 1.118,0.559 -1.587,-0.381 -1.788,-0.604 -2.324,-1.229 0,-0.045 -0.134,-0.135 -0.134,-0.156 -0.715,-0.805 -1.609,-2.19 -1.922,-3.464 -0.112,-0.447 -0.224,-0.916 -0.089,-1.363 0.581,-2.101 1.854,-2.905 3.128,-3.775 0.313,-0.225 0.626,-0.426 0.916,-0.649 0.715,-0.559 0.894,-2.012 1.05,-2.838 -0.29,1.006 -0.603,2.258 -1.162,2.659 -0.29,0.224 -0.648,0.402 -0.938,0.604 -1.318,0.894 -2.637,1.743 -3.24,3.91 -0.134,0.56 -0.044,0.962 0.089,1.498 0.335,1.317 1.229,2.748 1.989,3.597l0.134,0.135c0.335,0.381 0.76,0.67 1.274,0.871a5.945,5.945 0,0 1,-1.296 -0.469c-2.078,-1.005 -3.463,-3.173 -3.553,-4.939 -0.179,-3.597 1.542,-4.647 3.151,-5.966 0.894,-0.737 2.145,-1.095 2.86,-2.413 0.134,-0.291 0.224,-0.916 0.045,-1.587 -0.067,-0.224 -0.402,-1.028 -0.537,-1.207l1.989,0.872c-0.044,0.938 -0.067,1.698 0.112,2.391 0.2,0.76 1.184,1.854 1.586,3.129 0.783,2.41 0.583,5.561 0.023,8.019z" + +/** The glyph for [action]; null means it's drawn with [OnionIcon]. */ +fun pillSymbolFor(action: Action): MaterialSymbol? = + when (action) { + Action.BACK -> MaterialSymbols.AutoMirrored.ArrowBack + Action.FORWARD -> MaterialSymbols.AutoMirrored.ArrowForward + Action.RELOAD -> MaterialSymbols.Refresh + Action.STOP -> MaterialSymbols.Close + Action.FAVORITE -> MaterialSymbols.Star + Action.SHARE -> MaterialSymbols.Share + Action.BACK_TO_APP -> MaterialSymbols.Home + Action.COPY_LINK -> MaterialSymbols.ContentCopy + Action.EDIT_ADDRESS -> MaterialSymbols.Edit + Action.FIND_IN_PAGE -> MaterialSymbols.Search + Action.TEXT_SIZE -> MaterialSymbols.FormatSize + Action.DESKTOP_SITE -> MaterialSymbols.DesktopWindows + Action.ADD_TO_HOME_SCREEN -> MaterialSymbols.AddToHomeScreen + Action.OPEN_IN_BROWSER_APP -> MaterialSymbols.OpenInBrowser + Action.OPEN_FULL_SCREEN -> MaterialSymbols.OpenInFull + Action.TOR -> null + Action.ACCESS_INFO -> MaterialSymbols.Shield + Action.SITE_SETTINGS -> MaterialSymbols.Tune + Action.CONSOLE -> MaterialSymbols.Code + } + +fun pillLabelFor( + action: Action, + isFavorite: Boolean = false, +): StringResource = + when (action) { + Action.BACK -> Res.string.browser_pill_back + Action.FORWARD -> Res.string.browser_pill_forward + Action.RELOAD -> Res.string.browser_pill_reload + Action.STOP -> Res.string.browser_pill_stop + Action.FAVORITE -> if (isFavorite) Res.string.browser_pill_favorite_remove else Res.string.browser_pill_favorite_add + Action.SHARE -> Res.string.browser_pill_share + Action.BACK_TO_APP -> Res.string.browser_pill_back_to_app + Action.COPY_LINK -> Res.string.browser_pill_copy + Action.EDIT_ADDRESS -> Res.string.browser_pill_edit_address + Action.FIND_IN_PAGE -> Res.string.browser_pill_find + Action.TEXT_SIZE -> Res.string.browser_pill_text_size + Action.DESKTOP_SITE -> Res.string.browser_pill_desktop + Action.ADD_TO_HOME_SCREEN -> Res.string.browser_pill_add_home + Action.OPEN_IN_BROWSER_APP -> Res.string.browser_pill_other_browser + Action.OPEN_FULL_SCREEN -> Res.string.browser_pill_full_screen + Action.TOR -> Res.string.browser_pill_tor_title + Action.ACCESS_INFO -> Res.string.browser_pill_access + Action.SITE_SETTINGS -> Res.string.browser_pill_site_settings + Action.CONSOLE -> Res.string.browser_pill_console + } + +/** + * The short label a page-action tile shows under its icon (tiles are ~88dp wide); the full + * [pillLabelFor] wording stays the accessibility description. + */ +fun pillTileLabelFor(action: Action): StringResource = + when (action) { + Action.COPY_LINK -> Res.string.browser_pill_tile_copy + Action.FIND_IN_PAGE -> Res.string.browser_pill_tile_find + Action.TEXT_SIZE -> Res.string.browser_pill_tile_text + Action.DESKTOP_SITE -> Res.string.browser_pill_tile_desktop + Action.ADD_TO_HOME_SCREEN -> Res.string.browser_pill_tile_home + Action.OPEN_IN_BROWSER_APP -> Res.string.browser_pill_tile_other + Action.OPEN_FULL_SCREEN -> Res.string.browser_pill_tile_full + else -> pillLabelFor(action) + } + +fun securityLabel(security: Security): StringResource = + when (security) { + Security.TOR -> Res.string.browser_pill_security_tor + Security.HTTPS -> Res.string.browser_pill_security_https + Security.HTTP -> Res.string.browser_pill_security_http + Security.SANDBOX -> Res.string.browser_pill_security_sandbox + } + +fun permissionLabel(permission: BrowserSitePermission): StringResource = + when (permission) { + BrowserSitePermission.CAMERA -> Res.string.browser_pill_permission_camera + BrowserSitePermission.MICROPHONE -> Res.string.browser_pill_permission_microphone + BrowserSitePermission.LOCATION -> Res.string.browser_pill_permission_location + } + +fun permissionSymbol(permission: BrowserSitePermission): MaterialSymbol = + when (permission) { + BrowserSitePermission.CAMERA -> MaterialSymbols.Videocam + BrowserSitePermission.MICROPHONE -> MaterialSymbols.Mic + BrowserSitePermission.LOCATION -> MaterialSymbols.LocationOn + } + +/** The colour that signals [security]: the error tone for plain HTTP, the Tor accent for onion routing. */ +@Composable +fun securityTint(security: Security): Color = + when (security) { + Security.HTTP -> MaterialTheme.colorScheme.error + Security.TOR -> MaterialTheme.colorScheme.tertiary + Security.SANDBOX -> MaterialTheme.colorScheme.primary + Security.HTTPS -> MaterialTheme.colorScheme.onSurfaceVariant + } + +/** Draws [action]'s icon (the onion for Tor). */ +@Composable +fun PillActionIcon( + action: Action, + tint: Color, + size: Dp = 24.dp, + filled: Boolean = false, + contentDescription: String? = null, +) { + val symbol = pillSymbolFor(action) + if (symbol != null) { + Icon(symbol, contentDescription = contentDescription, modifier = Modifier.size(size), tint = tint, filled = filled) + } else { + Material3Icon(OnionIcon, contentDescription = contentDescription, modifier = Modifier.size(size), tint = tint) + } +} + +/** Draws the badge for [security] in its signal colour. */ +@Composable +fun SecurityIcon( + security: Security, + size: Dp = 18.dp, + tint: Color = securityTint(security), +) { + when (security) { + Security.TOR -> Material3Icon(OnionIcon, contentDescription = null, modifier = Modifier.size(size), tint = tint) + Security.HTTPS -> Icon(MaterialSymbols.Lock, contentDescription = null, modifier = Modifier.size(size), tint = tint) + Security.HTTP -> Icon(MaterialSymbols.NoEncryption, contentDescription = null, modifier = Modifier.size(size), tint = tint) + Security.SANDBOX -> Icon(MaterialSymbols.Shield, contentDescription = null, modifier = Modifier.size(size), tint = tint, filled = true) + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt index 595b7cf845..6920115b06 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt @@ -77,11 +77,13 @@ object MaterialSymbols { val CloudSync = MaterialSymbol("\uEB5A") val CloudUpload = MaterialSymbol("\uE2C3") val Code = MaterialSymbol("\uE86F") + val Cookie = MaterialSymbol("\uEAAC") val Collections = MaterialSymbol("\uE3D3") val CollectionsBookmark = MaterialSymbol("\uE431") val Commit = MaterialSymbol("\uEAF5") val ContentCopy = MaterialSymbol("\uE14D") val ContentPaste = MaterialSymbol("\uE14F") + val ContentPasteGo = MaterialSymbol("\uEA8E") val CurrencyBitcoin = MaterialSymbol("\uEBC5") val Dashboard = MaterialSymbol("\uE871") val DateRange = MaterialSymbol("\uE916") @@ -173,6 +175,7 @@ object MaterialSymbols { val News = MaterialSymbol("\uE032") val NoAccounts = MaterialSymbol("\uF03E") val NoEncryption = MaterialSymbol("\uF03F") + val NorthWest = MaterialSymbol("\uF1E2", autoMirror = true) val Notifications = MaterialSymbol("\uE7F5") val NotificationsOff = MaterialSymbol("\uE7F6") val Numbers = MaterialSymbol("\uEAC7") diff --git a/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillRenderTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillRenderTest.kt new file mode 100644 index 0000000000..3ac0d504e9 --- /dev/null +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillRenderTest.kt @@ -0,0 +1,96 @@ +/* + * 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.commons.browser.ui.pill + +import androidx.compose.runtime.Composable +import androidx.compose.ui.ImageComposeScene +import androidx.compose.ui.unit.Density +import org.jetbrains.skia.EncodedImageFormat +import java.io.File +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * Renders every browser-pill prototype offscreen (no device, no display) through `ImageComposeScene`, in the + * dark and light Amethyst themes side by side, and writes each to `commonsUI/build/browser-pill/<name>.png`. + * + * It exists so the redesign can be *looked at* while it's built — see + * `amethyst/plans/2026-09-26-browser-ui-review.md` — and it fails if a screen throws or draws nothing. + */ +class BrowserPillRenderTest { + private val outDir = File("build/browser-pill").apply { mkdirs() } + + private fun render( + name: String, + widthDp: Int, + heightDp: Int, + content: @Composable () -> Unit, + ) { + val density = 2f + val width = (widthDp * density).toInt() + val height = (heightDp * density).toInt() + val scene = ImageComposeScene(width = width, height = height, density = Density(density), content = content) + try { + // Strings and fonts load asynchronously from compose resources: let a few frames settle. + var image = scene.render(0) + repeat(SETTLE_FRAMES) { frame -> + Thread.sleep(FRAME_MILLIS) + image = scene.render((frame + 1) * FRAME_MILLIS * 1_000_000L) + } + val png = image.encodeToData(EncodedImageFormat.PNG) ?: error("could not encode $name") + File(outDir, "$name.png").writeBytes(png.bytes) + + val pixels = image.peekPixels() ?: error("no pixels for $name") + val distinct = HashSet<Int>() + for (y in 0 until height step 7) for (x in 0 until width step 7) distinct += pixels.getColor(x, y) + assertTrue(distinct.size > 20, "$name rendered almost nothing (${distinct.size} colours)") + } finally { + scene.close() + } + } + + @Test fun expanded() = render("01-expanded", 820, 1000) { BrowserPillExpandedPreview() } + + @Test fun torOutOfScope() = render("02-tor-out-of-scope", 820, 1100) { BrowserPillTorOutOfScopePreview() } + + @Test fun addressEditor() = render("03-address-editor", 820, 700) { BrowserPillAddressEditorPreview() } + + @Test fun napplet() = render("04-napplet", 820, 800) { BrowserPillNappletPreview() } + + @Test fun handles() = render("05-handles", 820, 160) { PillHandlesPreview() } + + @Test fun find() = render("06-find", 820, 220) { FindInPagePreview() } + + @Test fun console() = render("07-console", 820, 380) { ConsoleSheetPreview() } + + @Test fun permission() = render("08-permission", 820, 700) { PermissionPromptPreview() } + + @Test fun dialog() = render("09-dialog", 820, 560) { PageDialogPreview() } + + @Test fun leave() = render("10-leave", 820, 420) { LeaveSiteDialogPreview() } + + @Test fun pageInfo() = render("11-page-info", 820, 1250) { PageInfoPreview() } + + private companion object { + const val SETTLE_FRAMES = 12 + const val FRAME_MILLIS = 60L + } +} From d52d54ac3f1f74c95c99f761fa1cad7c2d19680a Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 18:36:00 +0000 Subject: [PATCH 10/60] feat(browser): draw every browser surface with the Compose pill components Embedded tabs, the full-screen web browser and the nsite/napplet windows now share one set of Compose components (commonsUI browser/ui/pill): - Embedded tabs: EmbeddedTabChrome carries a BrowserPillUi plus one event callback. The tab layer draws BrowserPill, FindInPagePill and ConsoleSheet. The address editor suggests favorites and history. - Embedded page dialogs: PageDialogCard, PermissionPromptCard (now with "Only this time") and PageInfoSheet. Page info reports the certificate as fields, and site permissions can be edited in place. - Full-screen windows: BrowserChromeHost hosts the same composables in ComposeViews over the WebView, replacing the hand-built View chrome. - Clear site data asks for confirmation first. Removed the old renderers: NappletControlSheet, BrowserFindBar, NappletConsolePanel, BrowserJsDialogs, BrowserChromeLabels, TopControlSheet, EmbeddedFindBar, BottomConsoleSheet and EmbeddedPageDialogs, plus the strings only they used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TEkj7Eo2xbidVHAoF8GZKQ --- .../plans/2026-09-26-browser-pwa-parity.md | 4 + .../plans/2026-09-26-browser-ui-review.md | 46 +- .../loggedIn/browser/EmbeddedPageDialogs.kt | 163 ----- .../loggedIn/browser/EmbeddedPageRequests.kt | 7 +- .../browser/EmbeddedWebAppController.kt | 44 +- .../screen/loggedIn/browser/WebAppScreen.kt | 222 ++++-- .../loggedIn/embed/BottomConsoleSheet.kt | 237 ------- .../ui/screen/loggedIn/embed/ConsoleBridge.kt | 14 +- .../screen/loggedIn/embed/EmbeddedFindBar.kt | 123 ---- .../loggedIn/embed/EmbeddedTabChrome.kt | 34 +- .../screen/loggedIn/embed/EmbeddedTabLayer.kt | 174 +++-- .../screen/loggedIn/embed/TopControlSheet.kt | 459 ------------- .../loggedIn/favorites/NostrAppScreen.kt | 34 +- .../src/androidMain/res/values/strings.xml | 59 +- .../commons/browser/ui/pill/AddressEditor.kt | 20 +- .../browser/ui/pill/BrowserChromeTheme.kt | 35 + .../commons/browser/ui/pill/BrowserPill.kt | 13 +- .../browser/ui/pill/BrowserPillModel.kt | 3 + .../browser/ui/pill/BrowserPillPreviews.kt | 2 +- .../commons/browser/ui/pill/ConsoleSheet.kt | 7 + .../commons/browser/ui/pill/PillComponents.kt | 13 +- nappletHost/build.gradle.kts | 7 + .../amethyst/napplethost/BrowserChromeHost.kt | 388 +++++++++++ .../napplethost/BrowserChromeLabels.kt | 157 ----- .../amethyst/napplethost/BrowserFindBar.kt | 197 ------ .../amethyst/napplethost/BrowserJsDialogs.kt | 182 ----- .../amethyst/napplethost/BrowserWebTools.kt | 66 +- .../napplethost/NappletBrowserActivity.kt | 392 ++++++----- .../napplethost/NappletBrowserContract.kt | 11 +- .../napplethost/NappletBrowserService.kt | 7 +- .../napplethost/NappletConsolePanel.kt | 302 -------- .../napplethost/NappletControlSheet.kt | 647 ------------------ .../napplethost/NappletHostActivity.kt | 185 ++--- 33 files changed, 1212 insertions(+), 3042 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedPageDialogs.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/BottomConsoleSheet.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedFindBar.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/TopControlSheet.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserChromeTheme.kt create mode 100644 nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserChromeHost.kt delete mode 100644 nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserChromeLabels.kt delete mode 100644 nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserFindBar.kt delete mode 100644 nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserJsDialogs.kt delete mode 100644 nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletConsolePanel.kt delete mode 100644 nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletControlSheet.kt diff --git a/amethyst/plans/2026-09-26-browser-pwa-parity.md b/amethyst/plans/2026-09-26-browser-pwa-parity.md index 115e1678a3..8c01bd3c6d 100644 --- a/amethyst/plans/2026-09-26-browser-pwa-parity.md +++ b/amethyst/plans/2026-09-26-browser-pwa-parity.md @@ -292,6 +292,10 @@ nsite/napplet hosts (sandbox profile permitting). What shipped, where it lives, and what was deliberately left out. +> The two renderers described below (`TopControlSheet`, `NappletControlSheet` and their find, console and +> dialog views) were later replaced by one set of Compose components. See +> `2026-09-26-browser-ui-review.md` §5. + **Shared layout.** `commons/…/browser/BrowserChrome.kt` decides which actions the top pill shows, and in what order, for every surface (web / nsite / napplet × embedded / full screen), plus the security badge, the scope check, text-zoom steps, the desktop user agent and the theme-colour parser. It is covered by diff --git a/amethyst/plans/2026-09-26-browser-ui-review.md b/amethyst/plans/2026-09-26-browser-ui-review.md index 58d0284e59..f87c24a31b 100644 --- a/amethyst/plans/2026-09-26-browser-ui-review.md +++ b/amethyst/plans/2026-09-26-browser-ui-review.md @@ -1,9 +1,9 @@ # Browser surfaces: UI review and redesign -Status: **review + Compose prototypes**. The prototypes live in +Status: **shipped** (see §5). The components live in `commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/`. They are rendered offscreen by `BrowserPillRenderTest` (commonsUI jvmTest), which writes PNGs to -`commonsUI/build/browser-pill/`. None of it is wired into the app yet (see §5). +`commonsUI/build/browser-pill/`. Follows `2026-09-26-browser-pwa-parity.md`. That plan settled *what* the browser does. This one is about how it looks and feels. @@ -222,15 +222,35 @@ A sheet with: | Spacing | 4dp grid, 16dp sheet padding, 8dp between tiles | | Type | title `titleMedium`, origin `titleSmall`, tiles `labelMedium`, supporting `bodySmall` | -## 5. Next steps +## 5. What shipped -1. Wire the prototypes in: `TopControlSheet` → `BrowserPill`, `EmbeddedFindBar` → `FindInPagePill`, - `BottomConsoleSheet` → `ConsoleSheet`, and the embedded dialogs → `PermissionPromptCard` / - `PageDialogCard` / `PageInfoSheet`. -2. Host the same composables in the full-screen window through `ComposeView`, and delete - `NappletControlSheet`, `BrowserFindBar`, `NappletConsolePanel` and `BrowserJsDialogs`. -3. Move the pill's labels from the `commons` Android resources to the Compose catalogue (the - prototypes already use `Res.string.browser_pill_*`). -4. Add "Only this time" to the permission flow (the registry already treats "no answer" as - not-remembered). -5. Confirm before clearing site data in both surfaces. +Every browser surface now draws the components in `commonsUI/…/browser/ui/pill/`; the hand-built chrome +is gone. + +- **Embedded tabs** (`EmbeddedTabLayer`): `EmbeddedTabChrome` now carries a `BrowserPillUi` and one + `onEvent(BrowserPillEvent)` callback. The layer draws `BrowserPill`, `FindInPagePill` and + `ConsoleSheet`, and handles find and the console itself. The address editor gets suggestions from + favorites and history (`OmniboxSuggestions`), and "Paste and go" checks only the clip's type. +- **Embedded page dialogs** (`WebAppScreen`): `PageDialogCard`, `PermissionPromptCard` and + `PageInfoSheet` in Compose `Dialog`s. `MSG_PAGE_INFO` now sends the certificate fields + (`KEY_CERT_ISSUED_TO` / `_BY` / `_VALID_UNTIL`) instead of a paragraph of text. Permissions in page + info are edited in place, straight into `WebSitePermissionRegistry`. +- **Full-screen windows** (`NappletBrowserActivity`, `NappletHostActivity`): `BrowserChromeHost` hosts + the same composables in two `ComposeView`s over the page. The top view grows to fill the window only + while the pill is open, so it can catch taps outside the pill; the bottom view holds find or the + console. Dialogs, the permission prompt and page info are Compose `Dialog`s. The browser asks the + broker for the site's decisions before showing page info. `:nappletHost` now applies the Compose + compiler and links the same JetBrains Compose libraries (Apache-2.0) the app already ships. +- **Permissions:** Allow while visiting (remembered), Only this time (granted, not remembered), and + Don't allow (remembered). Dismissing the prompt denies the request once and remembers nothing. +- **Clear site data** asks for confirmation inline, in both surfaces. +- **Removed:** `NappletControlSheet`, `BrowserFindBar`, `NappletConsolePanel`, `BrowserJsDialogs`, + `BrowserChromeLabels`, `TopControlSheet`, `EmbeddedFindBar`, `BottomConsoleSheet`, + `EmbeddedPageDialogs`, `ConsoleLogEntry` and `BrowserWebTools.pageInfo`, plus the Android strings only + they used. + +Still open: +- The sandboxed apps' "What it can access" is still a platform `AlertDialog`. It lists launch + capabilities, not browser state. +- The embedded nsite/napplet tab has no find or text-size plumbing yet (`hasFind` / `hasTextSize` are + off there). diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedPageDialogs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedPageDialogs.kt deleted file mode 100644 index e7b4c6821e..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedPageDialogs.kt +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.browser - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.browser.BrowserChrome -import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.commons.R as CommonsR - -/** - * An embedded page's `alert` / `confirm` / `prompt` / `beforeunload`, drawn by the main process over the tab - * (the provider's WebView has no window to draw one). Titled with the page's host ("example.com says") so a - * page can't pass its dialog off as Amethyst's own, and offering "Block dialogs from this page" from the - * page's second dialog on — the same rules as the full-screen browser. - */ -@Composable -fun EmbeddedJsDialogView( - dialog: EmbeddedJsDialog, - onAnswer: (confirmed: Boolean, text: String?, block: Boolean) -> Unit, -) { - var text by remember(dialog.id) { mutableStateOf(dialog.defaultValue) } - val leave = dialog.type == EmbeddedJsDialog.Type.BEFORE_UNLOAD - val host = dialog.url?.let { BrowserChrome.originOf(it) }?.let(BrowserChrome::displayHost) - AlertDialog( - onDismissRequest = { onAnswer(false, null, false) }, - title = { - Text( - when { - leave -> stringRes(CommonsR.string.browser_js_leave_title) - host != null -> stringRes(CommonsR.string.browser_js_dialog_title, host) - else -> stringRes(CommonsR.string.browser_js_dialog_title_generic) - }, - ) - }, - text = { - Column(Modifier.verticalScroll(rememberScrollState())) { - // The page's own beforeunload text is ignored, as in every current browser (it was abused). - Text(if (leave) stringRes(CommonsR.string.browser_js_leave_message) else dialog.message) - if (dialog.type == EmbeddedJsDialog.Type.PROMPT) { - Spacer(Modifier.height(12.dp)) - OutlinedTextField(value = text, onValueChange = { text = it }, singleLine = true, modifier = Modifier.fillMaxWidth()) - } - } - }, - confirmButton = { - TextButton(onClick = { onAnswer(true, if (dialog.type == EmbeddedJsDialog.Type.PROMPT) text else null, false) }) { - Text(if (leave) stringRes(CommonsR.string.browser_js_leave) else stringRes(android.R.string.ok)) - } - }, - dismissButton = { - Row { - if (dialog.offerBlock) { - // A blocked page may not keep the user on it, so blocking a beforeunload means leaving. - TextButton(onClick = { onAnswer(leave, null, true) }) { Text(stringRes(CommonsR.string.browser_js_dialog_block)) } - } - if (dialog.type != EmbeddedJsDialog.Type.ALERT) { - TextButton(onClick = { onAnswer(false, null, false) }) { Text(stringRes(android.R.string.cancel)) } - } - } - }, - ) -} - -/** - * Chrome's permission bubble for an embedded page: "<host> wants to — use your camera — Block / Allow". - * Dismissing it denies this once without remembering anything. - */ -@Composable -fun EmbeddedPermissionPrompt( - origin: String, - permissions: Set<BrowserSitePermission>, - onAnswer: (allow: Boolean?) -> Unit, -) { - AlertDialog( - onDismissRequest = { onAnswer(null) }, - title = { Text(stringRes(CommonsR.string.browser_permission_title, BrowserChrome.displayHost(origin))) }, - text = { - Column { - permissions.forEach { permission -> - Text( - "• " + - stringRes( - when (permission) { - BrowserSitePermission.CAMERA -> CommonsR.string.browser_permission_camera - BrowserSitePermission.MICROPHONE -> CommonsR.string.browser_permission_microphone - BrowserSitePermission.LOCATION -> CommonsR.string.browser_permission_location - }, - ), - ) - } - } - }, - confirmButton = { TextButton(onClick = { onAnswer(true) }) { Text(stringRes(CommonsR.string.browser_permission_allow)) } }, - dismissButton = { TextButton(onClick = { onAnswer(false) }) { Text(stringRes(CommonsR.string.browser_permission_block)) } }, - ) -} - -/** Chrome's page-info sheet for an embedded page: connection, Tor, certificate; site settings; clear data. */ -@Composable -fun EmbeddedPageInfoDialog( - host: String, - info: String?, - onPermissions: (() -> Unit)?, - onClearData: () -> Unit, - onDismiss: () -> Unit, -) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(host) }, - text = { Text(info ?: "…") }, - confirmButton = { TextButton(onClick = onDismiss) { Text(stringRes(android.R.string.ok)) } }, - dismissButton = { - Row { - TextButton(onClick = { - onClearData() - onDismiss() - }) { Text(stringRes(CommonsR.string.browser_page_info_clear_data)) } - if (onPermissions != null) { - TextButton(onClick = { - onPermissions() - onDismiss() - }) { Text(stringRes(CommonsR.string.browser_page_info_permissions)) } - } - } - }, - ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedPageRequests.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedPageRequests.kt index 90d9ff904b..65eedabf6d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedPageRequests.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedPageRequests.kt @@ -21,19 +21,18 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.browser import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission +import com.vitorpamplona.amethyst.commons.browser.ui.pill.PageDialogType /** A JS dialog an embedded page opened, waiting for the user (the page's script is paused meanwhile). */ data class EmbeddedJsDialog( val id: Long, - val type: Type, + val type: PageDialogType, val url: String?, val message: String, val defaultValue: String, /** Offer "Block dialogs from this page" (from the page's second dialog on, as Chrome does). */ val offerBlock: Boolean, -) { - enum class Type { ALERT, CONFIRM, PROMPT, BEFORE_UNLOAD } -} +) /** A camera / microphone / location request from an embedded page, waiting for an answer. */ data class EmbeddedPermissionRequest( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt index 427b3376d5..4cd48b1181 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt @@ -42,11 +42,13 @@ import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory import androidx.privacysandbox.ui.client.view.SandboxedSdkView import androidx.privacysandbox.ui.core.SandboxedUiAdapter import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission +import com.vitorpamplona.amethyst.commons.browser.ui.pill.CertificateInfo +import com.vitorpamplona.amethyst.commons.browser.ui.pill.ConsoleLine +import com.vitorpamplona.amethyst.commons.browser.ui.pill.PageDialogType import com.vitorpamplona.amethyst.napplet.NappletWebViewProfiles import com.vitorpamplona.amethyst.napplet.WebFileChooserCoordinator import com.vitorpamplona.amethyst.napplethost.NappletBrowserContract import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.ConsoleBridge -import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.ConsoleLogEntry import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedImeBridge import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedLoadStatus import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedMagnifierProbe @@ -102,7 +104,7 @@ class EmbeddedWebAppController( override var onLoadStatusChanged: ((EmbeddedLoadStatus) -> Unit)? = null /** JavaScript console output received from the embedded WebView, capped at [MAX_CONSOLE_LOGS] entries. */ - override val consoleLogs = mutableStateListOf<ConsoleLogEntry>() + override val consoleLogs = mutableStateListOf<ConsoleLine>() override fun clearConsoleLogs() = consoleLogs.clear() @@ -126,8 +128,8 @@ class EmbeddedWebAppController( /** The camera / microphone / location request the page is waiting on, if any. */ val pendingPermission = mutableStateOf<EmbeddedPermissionRequest?>(null) - /** Page-info text for the page on screen, once requested. */ - val pageInfo = mutableStateOf<String?>(null) + /** The certificate of the page on screen, once page info asked for it (null for none, or not yet). */ + val pageCertificate = mutableStateOf<CertificateInfo?>(null) /** A main-frame load is in flight (the pill's reload button becomes stop). */ val isLoading = mutableStateOf(false) @@ -284,7 +286,7 @@ class EmbeddedWebAppController( val source = msg.data?.getString(NappletBrowserContract.KEY_CONSOLE_SOURCE).orEmpty() val line = msg.data?.getInt(NappletBrowserContract.KEY_CONSOLE_LINE, 0) ?: 0 if (consoleLogs.size >= MAX_CONSOLE_LOGS) consoleLogs.removeAt(0) - consoleLogs.add(ConsoleLogEntry(level, message, source, line)) + consoleLogs.add(ConsoleLine(consoleLevelOf(level), message, source, line)) } NappletBrowserContract.MSG_FILE_CHOOSER_REQUEST -> { val data = msg.data ?: return true @@ -309,7 +311,17 @@ class EmbeddedWebAppController( val data = msg.data ?: return true _findResult.value = FindResult(data.getInt(NappletBrowserContract.KEY_FIND_ACTIVE), data.getInt(NappletBrowserContract.KEY_FIND_TOTAL)) } - NappletBrowserContract.MSG_PAGE_INFO -> pageInfo.value = msg.data?.getString(NappletBrowserContract.KEY_PAGE_INFO) + NappletBrowserContract.MSG_PAGE_INFO -> { + val data = msg.data ?: return true + pageCertificate.value = + data.getString(NappletBrowserContract.KEY_CERT_ISSUED_TO)?.let { issuedTo -> + CertificateInfo( + issuedTo = issuedTo, + issuedBy = data.getString(NappletBrowserContract.KEY_CERT_ISSUED_BY).orEmpty(), + validUntil = data.getString(NappletBrowserContract.KEY_CERT_VALID_UNTIL).orEmpty(), + ) + } + } NappletBrowserContract.MSG_JS_DIALOG -> { val data = msg.data ?: return true val id = data.getLong(NappletBrowserContract.KEY_DIALOG_ID) @@ -323,10 +335,10 @@ class EmbeddedWebAppController( id = id, type = when (data.getString(NappletBrowserContract.KEY_DIALOG_TYPE)) { - "confirm" -> EmbeddedJsDialog.Type.CONFIRM - "prompt" -> EmbeddedJsDialog.Type.PROMPT - "beforeunload" -> EmbeddedJsDialog.Type.BEFORE_UNLOAD - else -> EmbeddedJsDialog.Type.ALERT + "confirm" -> PageDialogType.CONFIRM + "prompt" -> PageDialogType.PROMPT + "beforeunload" -> PageDialogType.BEFORE_UNLOAD + else -> PageDialogType.ALERT }, url = data.getString(NappletBrowserContract.KEY_URL), message = data.getString(NappletBrowserContract.KEY_DIALOG_MESSAGE).orEmpty(), @@ -439,7 +451,7 @@ class EmbeddedWebAppController( fun clearSiteData() = send(NappletBrowserContract.MSG_CLEAR_SITE_DATA) {} fun requestPageInfo() { - pageInfo.value = null + pageCertificate.value = null send(NappletBrowserContract.MSG_PAGE_INFO_REQUEST) {} } @@ -513,3 +525,13 @@ class EmbeddedWebAppController( private const val MAX_CONSOLE_LOGS = 200 } } + +/** Maps the provider's console level (WebView's `ConsoleMessage.MessageLevel` name) onto the chrome's. */ +private fun consoleLevelOf(level: String): ConsoleLine.Level = + when (level) { + "ERROR" -> ConsoleLine.Level.ERROR + "WARNING" -> ConsoleLine.Level.WARNING + "DEBUG" -> ConsoleLine.Level.DEBUG + "TIP" -> ConsoleLine.Level.INFO + else -> ConsoleLine.Level.LOG + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/WebAppScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/WebAppScreen.kt index 4c26663a39..2abb3ecdcc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/WebAppScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/WebAppScreen.kt @@ -30,7 +30,9 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -49,6 +51,9 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties import androidx.core.content.ContextCompat import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -57,6 +62,13 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.browser.BrowserChrome import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission import com.vitorpamplona.amethyst.commons.browser.OmniboxInput +import com.vitorpamplona.amethyst.commons.browser.OmniboxSuggestions +import com.vitorpamplona.amethyst.commons.browser.ui.pill.AddressSuggestion +import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPillEvent +import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPillUi +import com.vitorpamplona.amethyst.commons.browser.ui.pill.PageDialogCard +import com.vitorpamplona.amethyst.commons.browser.ui.pill.PageInfoSheet +import com.vitorpamplona.amethyst.commons.browser.ui.pill.PermissionPromptCard import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.navigation.favoriteIds @@ -64,6 +76,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.browser_unsupported import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.favorites.BrowserHistoryRegistry import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher import com.vitorpamplona.amethyst.favorites.FavoriteAppsRegistry import com.vitorpamplona.amethyst.favorites.WebShortcuts @@ -171,71 +184,101 @@ private fun EmbeddedWebAppTab( browserOrigin(currentUrl)?.let { origin -> nav.nav(Route.ConnectedAppDetail("browser:$origin")) } } + fun onAction(action: BrowserChrome.Action) { + when (action) { + BrowserChrome.Action.BACK -> controller.back() + BrowserChrome.Action.FORWARD -> controller.forward() + BrowserChrome.Action.RELOAD -> controller.reload() + BrowserChrome.Action.STOP -> controller.stop() + BrowserChrome.Action.FAVORITE -> toggleFavorite() + BrowserChrome.Action.SHARE -> BrowserWebTools.share(context, pageTitle, null, currentUrl) + BrowserChrome.Action.BACK_TO_APP -> controller.backToScope(url) + BrowserChrome.Action.COPY_LINK -> BrowserWebTools.copyToClipboard(context, currentUrl) + BrowserChrome.Action.DESKTOP_SITE -> { + desktopSite = !desktopSite + controller.setDesktopSite(desktopSite) + } + BrowserChrome.Action.ADD_TO_HOME_SCREEN -> WebShortcuts.requestPin(context, currentUrl, pageTitle ?: hostLabel(currentUrl)) + BrowserChrome.Action.OPEN_IN_BROWSER_APP -> BrowserWebTools.openInOtherBrowser(context, currentUrl) + // The page the user is looking at, not the one the tab was pinned with. + BrowserChrome.Action.OPEN_FULL_SCREEN -> FavoriteAppLauncher.launchUrl(context, currentUrl) + BrowserChrome.Action.TOR -> { + torOn = !torOn + controller.setTor(torOn) + WebAppNetworkRegistry.set(currentUrl, torOn) + } + BrowserChrome.Action.SITE_SETTINGS -> openSiteSettings() + else -> Unit + } + } + + fun onNavigate(text: String) { + val resolved = OmniboxInput.resolve(text) ?: return + // .onion only resolves over Tor. + if (resolved.forceTor && proxyAvailable && !torOn) { + torOn = true + controller.setTor(true) + } + controller.navigate(resolved.url) + } + + // Favorites first, then history: what the address editor offers for what the user has typed. + val history by BrowserHistoryRegistry.history.collectAsStateWithLifecycle() + val candidates = + remember(apps, history) { + buildList { + apps.forEach { if (it is FavoriteApp.WebApp) add(OmniboxSuggestions.Candidate(it.url, it.label, isFavorite = true)) } + history.forEach { + add(OmniboxSuggestions.Candidate(it.url, it.title.ifBlank { it.host }, isFavorite = false, visitCount = it.visitCount, lastVisitedAt = it.lastVisitedAt)) + } + } + } + + val siteDecisions by WebSitePermissionRegistry.decisions.collectAsStateWithLifecycle() + val sitePermissions = remember(siteDecisions, currentUrl) { browserOrigin(currentUrl)?.let { siteDecisions[it] }.orEmpty() } + // Rebuilt only when a displayed value changes, so the tab layer isn't recomposed every frame. val chrome = - remember(currentUrl, pageTitle, canGoBack, canGoForward, isLoading, torOn, proxyAvailable, isFavorite, desktopSite, textZoom, controller) { + remember(currentUrl, pageTitle, canGoBack, canGoForward, isLoading, torOn, proxyAvailable, isFavorite, desktopSite, textZoom, sitePermissions, candidates, controller) { EmbeddedTabChrome( - title = pageTitle ?: hostLabel(currentUrl), - state = - BrowserChrome.State( - surface = BrowserChrome.Surface.WEB, - presentation = BrowserChrome.Presentation.EMBEDDED, - url = currentUrl, - startUrl = url, - canGoBack = canGoBack, - canGoForward = canGoForward, - isLoading = isLoading, - torOn = if (proxyAvailable) torOn else null, - hasSiteSettings = browserOrigin(currentUrl) != null, + ui = + BrowserPillUi( + title = pageTitle ?: hostLabel(currentUrl), + chrome = + BrowserChrome.State( + surface = BrowserChrome.Surface.WEB, + presentation = BrowserChrome.Presentation.EMBEDDED, + url = currentUrl, + startUrl = url, + canGoBack = canGoBack, + canGoForward = canGoForward, + isLoading = isLoading, + torOn = if (proxyAvailable) torOn else null, + hasSiteSettings = browserOrigin(currentUrl) != null, + ), + isFavorite = isFavorite, + desktopSite = desktopSite, + textZoom = textZoom, + sitePermissions = sitePermissions, ), - isFavorite = isFavorite, - desktopSite = desktopSite, - textZoom = textZoom, - onAction = { action -> - when (action) { - BrowserChrome.Action.BACK -> controller.back() - BrowserChrome.Action.FORWARD -> controller.forward() - BrowserChrome.Action.RELOAD -> controller.reload() - BrowserChrome.Action.STOP -> controller.stop() - BrowserChrome.Action.FAVORITE -> toggleFavorite() - BrowserChrome.Action.SHARE -> BrowserWebTools.share(context, pageTitle, null, currentUrl) - BrowserChrome.Action.BACK_TO_APP -> controller.backToScope(url) - BrowserChrome.Action.COPY_LINK -> BrowserWebTools.copyToClipboard(context, currentUrl) - BrowserChrome.Action.DESKTOP_SITE -> { - desktopSite = !desktopSite - controller.setDesktopSite(desktopSite) + onEvent = { event -> + when (event) { + is BrowserPillEvent.Action -> onAction(event.action) + is BrowserPillEvent.Navigate -> onNavigate(event.input) + is BrowserPillEvent.TextZoom -> { + textZoom = event.percent + controller.setTextZoom(event.percent) } - BrowserChrome.Action.ADD_TO_HOME_SCREEN -> WebShortcuts.requestPin(context, currentUrl, pageTitle ?: hostLabel(currentUrl)) - BrowserChrome.Action.OPEN_IN_BROWSER_APP -> BrowserWebTools.openInOtherBrowser(context, currentUrl) - // The page the user is looking at, not the one the tab was pinned with. - BrowserChrome.Action.OPEN_FULL_SCREEN -> FavoriteAppLauncher.launchUrl(context, currentUrl) - BrowserChrome.Action.TOR -> { - torOn = !torOn - controller.setTor(torOn) - WebAppNetworkRegistry.set(currentUrl, torOn) + BrowserPillEvent.CopyOrigin -> BrowserWebTools.copyToClipboard(context, currentUrl) + BrowserPillEvent.PageInfo -> { + controller.requestPageInfo() + showPageInfo = true } - BrowserChrome.Action.SITE_SETTINGS -> openSiteSettings() - else -> Unit + BrowserPillEvent.Close -> Unit } }, - onNavigate = { text -> - val resolved = OmniboxInput.resolve(text) - if (resolved != null) { - // .onion only resolves over Tor. - if (resolved.forceTor && proxyAvailable && !torOn) { - torOn = true - controller.setTor(true) - } - controller.navigate(resolved.url) - } - }, - onTextZoom = { percent -> - textZoom = percent - controller.setTextZoom(percent) - }, - onOriginTap = { - controller.requestPageInfo() - showPageInfo = true + suggestionsFor = { typed -> + OmniboxSuggestions.rank(typed, candidates, limit = 5).map { AddressSuggestion(it.label, it.url, it.isFavorite) } }, ) } @@ -259,7 +302,7 @@ private fun EmbeddedWebAppTab( // A fullscreen video inside the tab: back leaves fullscreen first, as in Chrome. BackHandler(enabled = isFullscreen) { controller.exitFullscreen() } - EmbeddedPageUi(controller, currentUrl, showPageInfo, onPageInfoDismiss = { showPageInfo = false }, onSiteSettings = ::openSiteSettings) + EmbeddedPageUi(controller, chrome.ui, showPageInfo, onPageInfoDismiss = { showPageInfo = false }) Scaffold( bottomBar = { @@ -280,22 +323,32 @@ private fun EmbeddedWebAppTab( /** * Everything an embedded page asks the user for, drawn by the main process because the provider has no * window: JS dialogs, camera / microphone / location prompts (remembered per origin in - * [WebSitePermissionRegistry], then Android's own runtime permission), and page info. + * [WebSitePermissionRegistry], then Android's own runtime permission), and page info — the shared + * [PageDialogCard], [PermissionPromptCard] and [PageInfoSheet]. */ @RequiresApi(Build.VERSION_CODES.R) @Composable private fun EmbeddedPageUi( controller: EmbeddedWebAppController, - currentUrl: String, + ui: BrowserPillUi, showPageInfo: Boolean, onPageInfoDismiss: () -> Unit, - onSiteSettings: () -> Unit, ) { val context = LocalContext.current val dialog by controller.pendingDialog dialog?.let { d -> - EmbeddedJsDialogView(d) { confirmed, text, block -> controller.answerDialog(d.id, confirmed, text, block) } + Dialog(onDismissRequest = { controller.answerDialog(d.id, confirmed = false) }) { + PageDialogCard( + type = d.type, + host = d.url?.let(::browserOrigin)?.let(::hostLabel), + security = ui.security, + message = d.message, + defaultValue = d.defaultValue, + offerBlock = d.offerBlock, + onResult = { confirmed, text, block -> controller.answerDialog(d.id, confirmed, text, block) }, + ) + } } // Android's runtime permission, asked only for what the user allowed the site to use. @@ -330,26 +383,47 @@ private fun EmbeddedPageUi( if (ask.isEmpty()) { LaunchedEffect(request.id) { grant(request.id, allowed) } } else { - EmbeddedPermissionPrompt(request.origin, ask) { allow -> - // null = dismissed without an answer: deny this once, remember nothing. - if (allow != null) { + // allow: grant now; remember: store the answer for the site ("Only this time" and a dismissal don't). + fun answer( + allow: Boolean, + remember: Boolean, + ) { + if (remember) { val decision = if (allow) BrowserSitePermission.Decision.ALLOW else BrowserSitePermission.Decision.BLOCK ask.forEach { WebSitePermissionRegistry.set(request.origin, it, decision) } } - grant(request.id, if (allow == true) allowed + ask else allowed) + grant(request.id, if (allow) allowed + ask else allowed) + } + Dialog(onDismissRequest = { answer(allow = false, remember = false) }) { + PermissionPromptCard( + host = hostLabel(request.origin), + security = ui.security, + permissions = ask, + onAllow = { answer(allow = true, remember = true) }, + onAllowOnce = { answer(allow = true, remember = false) }, + onDeny = { answer(allow = false, remember = true) }, + ) } } } if (showPageInfo) { - val info by controller.pageInfo - EmbeddedPageInfoDialog( - host = hostLabel(currentUrl), - info = info, - onPermissions = if (browserOrigin(currentUrl) != null) onSiteSettings else null, - onClearData = { controller.clearSiteData() }, - onDismiss = onPageInfoDismiss, - ) + val certificate by controller.pageCertificate + val origin = browserOrigin(ui.chrome.url) + Dialog(onDismissRequest = onPageInfoDismiss, properties = DialogProperties(usePlatformDefaultWidth = false)) { + Box(Modifier.fillMaxWidth().padding(16.dp), contentAlignment = Alignment.Center) { + PageInfoSheet( + ui = ui, + certificate = certificate, + onPermissionChange = { permission, decision -> origin?.let { WebSitePermissionRegistry.set(it, permission, decision) } }, + onClearSiteData = { + onPageInfoDismiss() + controller.clearSiteData() + }, + modifier = Modifier.widthIn(max = 560.dp), + ) + } + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/BottomConsoleSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/BottomConsoleSheet.kt deleted file mode 100644 index 5558cfdcae..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/BottomConsoleSheet.kt +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.embed - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.draggable -import androidx.compose.foundation.gestures.rememberDraggableState -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -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.HorizontalDivider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.commons.R as CommonsR - -/** - * A bottom **pull-up sheet** for JavaScript console output. Collapsed it's a small grabber at the - * bottom edge of the embedded surface. Pull it up (or tap) to reveal a scrollable log of console - * messages captured from the page via [WebChromeClient.onConsoleMessage]. The page can't draw over - * it (the surface is z-ordered below this layer). - * - * Mirrors [TopControlSheet]'s pattern but anchored at the bottom using a [Box] with - * [Alignment.BottomCenter]. - */ -@Composable -fun BottomConsoleSheet( - logs: List<ConsoleLogEntry>, - expanded: Boolean, - onExpandedChange: (Boolean) -> Unit, - onClear: () -> Unit, - modifier: Modifier = Modifier, -) { - Box(modifier = modifier, contentAlignment = Alignment.BottomCenter) { - // Column anchored at BottomCenter. Grabber sits at the top of the column so it rides up - // with the panel as it expands — standard bottom-sheet handle behaviour. - Column( - modifier = Modifier.align(Alignment.BottomCenter), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - // Grabber — the only touch target when collapsed; stays at the top of the panel when open - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = - Modifier - .clip(RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp)) - .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.6f)) - .clickable { onExpandedChange(!expanded) } - .draggable( - orientation = Orientation.Vertical, - state = - rememberDraggableState { delta -> - if (delta < -1f) { - onExpandedChange(true) - } else if (delta > 1f) { - onExpandedChange(false) - } - }, - ).padding(horizontal = 16.dp, vertical = 7.dp), - ) { - Spacer( - Modifier - .width(36.dp) - .height(5.dp) - .clip(RoundedCornerShape(50)) - .background(MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)), - ) - } - - AnimatedVisibility( - visible = expanded, - enter = expandVertically(expandFrom = Alignment.Bottom) + fadeIn(), - exit = shrinkVertically(shrinkTowards = Alignment.Bottom) + fadeOut(), - ) { - Surface( - color = MaterialTheme.colorScheme.surface.copy(alpha = 0.96f), - contentColor = MaterialTheme.colorScheme.onSurface, - // No elevation. This panel docks flush against the app's bottom navigation bar, so a - // shadowElevation just casts a shadow onto that bar — a seam that breaks the flush, - // background-matched look. The tonalElevation had no visual effect here anyway (the color - // isn't exactly colorScheme.surface, so Material never applies the surfaceTint); the - // shadow was the only thing clashing with the nav bar. The grabber + divider still - // separate the panel from the page above it. - tonalElevation = 0.dp, - shadowElevation = 0.dp, - shape = RoundedCornerShape(bottomStart = 0.dp, bottomEnd = 0.dp), - ) { - val maxHeight = (LocalConfiguration.current.screenHeightDp * 0.4f).dp - Column(Modifier.fillMaxWidth().heightIn(max = maxHeight)) { - Row( - Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - stringRes(CommonsR.string.browser_console_title, logs.size), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.weight(1f), - ) - TextButton(onClick = onClear) { - Text( - stringRes(CommonsR.string.browser_console_clear), - style = MaterialTheme.typography.labelSmall, - ) - } - } - HorizontalDivider() - val scrollState = rememberScrollState() - LaunchedEffect(logs.size) { scrollState.scrollTo(Int.MAX_VALUE) } - Column( - Modifier - .fillMaxWidth() - .verticalScroll(scrollState) - .padding(horizontal = 8.dp, vertical = 4.dp), - ) { - logs.forEach { entry -> - ConsoleLogRow(entry) - } - } - } - } - } - } - } -} - -@Composable -private fun ConsoleLogRow(entry: ConsoleLogEntry) { - val levelColor = consoleLevelColor(entry.level) - val dimColor = MaterialTheme.colorScheme.onSurfaceVariant - val srcShort = - entry.source - .substringAfterLast("/") - .substringAfterLast("\\") - .let { if (it.isBlank()) entry.source.takeLast(20) else it } - Row( - Modifier.fillMaxWidth().padding(vertical = 1.dp), - verticalAlignment = Alignment.Top, - ) { - Text( - consoleLevelChar(entry.level), - color = levelColor, - fontFamily = FontFamily.Monospace, - fontSize = 11.sp, - modifier = Modifier.width(14.dp), - ) - Spacer(Modifier.width(4.dp)) - Text( - buildAnnotatedString { - withStyle(SpanStyle(color = levelColor)) { - append(entry.message) - } - if (srcShort.isNotBlank()) { - withStyle(SpanStyle(color = dimColor)) { - append(" $srcShort:${entry.lineNumber}") - } - } - }, - fontFamily = FontFamily.Monospace, - fontSize = 11.sp, - modifier = Modifier.weight(1f), - overflow = TextOverflow.Visible, - ) - } -} - -@Composable -private fun consoleLevelColor(level: String): Color { - val warningAmber = if (isSystemInDarkTheme()) Color(0xFFFFB74D) else Color(0xFFE65100) - return when (level.uppercase()) { - "ERROR" -> MaterialTheme.colorScheme.error - "WARNING" -> warningAmber - "TIP" -> MaterialTheme.colorScheme.tertiary - "DEBUG" -> MaterialTheme.colorScheme.onSurfaceVariant - else -> MaterialTheme.colorScheme.onSurface - } -} - -private fun consoleLevelChar(level: String): String = - when (level.uppercase()) { - "ERROR" -> "E" - "WARNING" -> "W" - "TIP" -> "T" - "DEBUG" -> "D" - else -> "I" - } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/ConsoleBridge.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/ConsoleBridge.kt index 83875219dc..84e3bb70be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/ConsoleBridge.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/ConsoleBridge.kt @@ -21,21 +21,15 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.embed import androidx.compose.runtime.snapshots.SnapshotStateList +import com.vitorpamplona.amethyst.commons.browser.ui.pill.ConsoleLine /** * A surface controller that exposes JavaScript console output captured from the embedded WebView. - * The [consoleLogs] list is Compose snapshot state so [BottomConsoleSheet] recomposes as messages - * arrive. Implemented by [com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.EmbeddedWebAppController]. + * The [consoleLogs] list is Compose snapshot state so the console sheet recomposes as messages arrive. + * Implemented by [com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.EmbeddedWebAppController]. */ interface ConsoleBridge { - val consoleLogs: SnapshotStateList<ConsoleLogEntry> + val consoleLogs: SnapshotStateList<ConsoleLine> fun clearConsoleLogs() } - -data class ConsoleLogEntry( - val level: String, - val message: String, - val source: String, - val lineNumber: Int, -) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedFindBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedFindBar.kt deleted file mode 100644 index 6d2492c951..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedFindBar.kt +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.embed - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.icons.symbols.Icon -import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.commons.R as CommonsR - -/** - * Find in page for the active embedded tab, docked at the bottom of its bounds (the top edge belongs to the - * pill's grabber). Mirrors the full-screen browser's native find bar: query, "n/m", previous, next, close. - */ -@Composable -fun EmbeddedFindBar( - bridge: FindBridge, - onClose: () -> Unit, - modifier: Modifier = Modifier, -) { - var query by remember { mutableStateOf("") } - val result by bridge.findResult - val focus = remember { FocusRequester() } - LaunchedEffect(Unit) { focus.requestFocus() } - - fun close() { - bridge.find("") - onClose() - } - - Box(modifier, contentAlignment = Alignment.BottomCenter) { - Surface( - color = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface, - tonalElevation = 0.dp, - shadowElevation = 6.dp, - shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), - ) { - Row(Modifier.fillMaxWidth().padding(start = 4.dp, end = 4.dp), verticalAlignment = Alignment.CenterVertically) { - TextField( - value = query, - onValueChange = { - query = it - bridge.find(it) - }, - modifier = Modifier.weight(1f).focusRequester(focus), - singleLine = true, - placeholder = { Text(stringRes(CommonsR.string.browser_find_hint)) }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), - keyboardActions = KeyboardActions(onSearch = { bridge.findNext(true) }), - colors = - TextFieldDefaults.colors( - focusedContainerColor = Color.Transparent, - unfocusedContainerColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), - ) - val counts = result - if (query.isNotEmpty() && counts != null) { - Text( - if (counts.total > 0) stringRes(CommonsR.string.browser_find_count, counts.active + 1, counts.total) else "0/0", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = 4.dp), - ) - } - IconButton(onClick = { bridge.findNext(false) }) { - Icon(MaterialSymbols.KeyboardArrowUp, contentDescription = stringRes(CommonsR.string.browser_find_previous)) - } - IconButton(onClick = { bridge.findNext(true) }) { - Icon(MaterialSymbols.KeyboardArrowDown, contentDescription = stringRes(CommonsR.string.browser_find_next)) - } - IconButton(onClick = ::close) { - Icon(MaterialSymbols.Close, contentDescription = stringRes(CommonsR.string.browser_find_close)) - } - } - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabChrome.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabChrome.kt index 5e8817bbb4..200704f352 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabChrome.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabChrome.kt @@ -20,28 +20,24 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.embed -import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.browser.ui.pill.AddressSuggestion +import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPillEvent +import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPillUi /** - * The controls a running app surface offers through its top pull-down pill — described as plain data so - * [EmbeddedTabLayer] can draw the sheet over the (z-below) surface for the active tab. Deliberately not a - * corner pill: that's where a site usually puts the user's own avatar/menu, so the handle lives at the - * top-center instead. + * What a running app surface shows in its top pull-down pill, as plain data so [EmbeddedTabLayer] can draw + * the shared [com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPill] over the (z-below) surface + * for the active tab. Deliberately not a corner pill: that's where a site usually puts the user's own + * avatar/menu, so the handle lives at the top-center instead. * - * *Which* actions show, and in what order, comes from [BrowserChrome] with [state] — the same layout the - * full-screen browser's native sheet uses — so the two can't drift. Everything the user picks arrives in - * [onAction]; the console and find-in-page rows are handled by the layer itself. + * *Which* actions show comes from [BrowserPillUi.chrome] through + * [com.vitorpamplona.amethyst.commons.browser.BrowserChrome] — the same layout the full-screen windows + * use. Everything the user picks arrives in [onEvent]; find in page and the console are handled by the + * layer itself, since it draws them. */ data class EmbeddedTabChrome( - val title: String, - val state: BrowserChrome.State, - val isFavorite: Boolean = false, - val desktopSite: Boolean = false, - val textZoom: Int = BrowserChrome.DEFAULT_TEXT_ZOOM, - val onAction: (BrowserChrome.Action) -> Unit, - /** The user typed an address into "Edit address" and pressed Go. */ - val onNavigate: (String) -> Unit = {}, - val onTextZoom: (Int) -> Unit = {}, - /** The origin chip was tapped: page info (web) or the access summary (sandboxed apps). */ - val onOriginTap: () -> Unit = {}, + val ui: BrowserPillUi, + val onEvent: (BrowserPillEvent) -> Unit, + /** Address-editor suggestions for what the user has typed. */ + val suggestionsFor: (String) -> List<AddressSuggestion> = { emptyList() }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabLayer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabLayer.kt index 7ca4b02c25..ba49a8480c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabLayer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabLayer.kt @@ -83,7 +83,14 @@ import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.privacysandbox.ui.client.view.SandboxedSdkView +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome import com.vitorpamplona.amethyst.commons.browser.ui.EmbeddedLoadOverlay +import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPill +import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPillEvent +import com.vitorpamplona.amethyst.commons.browser.ui.pill.ConsoleLine +import com.vitorpamplona.amethyst.commons.browser.ui.pill.ConsoleSheet +import com.vitorpamplona.amethyst.commons.browser.ui.pill.FindInPagePill +import com.vitorpamplona.amethyst.napplethost.BrowserWebTools import kotlinx.coroutines.delay import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put @@ -126,7 +133,7 @@ private fun EmbeddedImeBridge.sendFieldOp( * * The surface is z-ordered *below* the client window (privacysandbox.ui locks it there), which still * forwards touch input to the provider yet lets Compose draw over it — so the active tab's - * [TopControlSheet] is rendered on top of the surface here. Each surface is wrapped in an + * pill ([com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPill]) is rendered on top of the surface here. Each surface is wrapped in an * [EmbeddedSurfaceTouchHolder] so a scroll gesture isn't stolen by a host-side ancestor (the * cross-process WebView can't defend its own gesture). * @@ -268,107 +275,117 @@ fun EmbeddedTabLayer(barFavoriteIds: List<String>) { } } - // The active tab's top pull-down sheet, drawn AFTER the surfaces so it sits on top of the - // (z-below) surface, anchored to the top of the active tab's reserved bounds. Its expanded state - // is owned here (reset per tab) so we can draw a full-area dismiss scrim behind the open sheet — - // while collapsed, only the small grabber is interactive and page taps pass through. + // The active tab's top pull-down pill, drawn AFTER the surfaces so it sits on top of the (z-below) + // surface, anchored to the top of the active tab's reserved bounds. Its expanded state is owned here + // (reset per tab) so we can draw a full-area dismiss scrim behind the open pill — while collapsed, + // only the small grabber is interactive and page taps pass through. val chrome = EmbeddedTabHost.activeChrome val consoleBridge = activeController as? ConsoleBridge - val consoleCount = consoleBridge?.consoleLogs?.size ?: 0 val findBridge = activeController as? FindBridge if (chrome != null && bounds.width > 0f && bounds.height > 0f) { - var sheetExpanded by remember(activeId) { mutableStateOf(false) } + var pillExpanded by remember(activeId) { mutableStateOf(false) } var consoleShowing by remember(activeId) { mutableStateOf(false) } - var consoleExpanded by remember(activeId) { mutableStateOf(false) } var findShowing by remember(activeId) { mutableStateOf(false) } + var findQuery by remember(activeId) { mutableStateOf("") } + val context = LocalContext.current - if (sheetExpanded) { + fun closeFind() { + if (findShowing) findBridge?.find("") + findShowing = false + findQuery = "" + } + + val tabModifier = + with(density) { + Modifier + .absoluteOffset( + (bounds.left - layerOrigin.x).toDp(), + (bounds.top - layerOrigin.y).toDp(), + ).size(bounds.width.toDp(), bounds.height.toDp()) + } + + if (pillExpanded) { + BackHandler { pillExpanded = false } Box( Modifier .fillMaxSize() .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, - ) { sheetExpanded = false }, + ) { pillExpanded = false }, ) } - with(density) { - TopControlSheet( - chrome = chrome, - expanded = sheetExpanded, - onExpandedChange = { sheetExpanded = it }, - consoleCount = consoleCount, + val consoleLogs = consoleBridge?.consoleLogs + val ui = + chrome.ui.copy( + chrome = chrome.ui.chrome.copy(hasFind = chrome.ui.chrome.hasFind && findBridge != null), consoleShowing = consoleShowing, - onConsole = - if (consoleBridge != null) { - { - if (consoleShowing) { - consoleShowing = false - } else { - // One bottom panel at a time: the console replaces the find bar. - if (findShowing) findBridge?.find("") - findShowing = false - consoleShowing = true - consoleExpanded = true - } - } - } else { - null - }, - onFind = - if (findBridge != null) { - { + consoleErrors = consoleLogs?.count { it.level == ConsoleLine.Level.ERROR } ?: 0, + ) + + Box(tabModifier) { + BrowserPill( + ui = ui, + expanded = pillExpanded, + onExpandedChange = { pillExpanded = it }, + onEvent = { event -> + val action = (event as? BrowserPillEvent.Action)?.action + when { + action == BrowserChrome.Action.FIND_IN_PAGE && findBridge != null -> { + // One bottom panel at a time: find replaces the console. consoleShowing = false findShowing = true } + action == BrowserChrome.Action.CONSOLE && consoleBridge != null -> { + if (!consoleShowing) closeFind() + consoleShowing = !consoleShowing + } + else -> chrome.onEvent(event) + } + }, + showClose = false, + suggestionsFor = chrome.suggestionsFor, + onPasteAndGo = + if (BrowserWebTools.clipboardHasText(context)) { + { + pillExpanded = false + BrowserWebTools.clipboardText(context)?.let { chrome.onEvent(BrowserPillEvent.Navigate(it)) } + } } else { null }, - modifier = - Modifier - .absoluteOffset( - (bounds.left - layerOrigin.x).toDp(), - (bounds.top - layerOrigin.y).toDp(), - ).width(bounds.width.toDp()), + modifier = Modifier.align(Alignment.TopCenter), ) - } - // Find in page: opened via the "Find in page" row in the top pull-down sheet. - if (findShowing && findBridge != null) { - BackHandler { - findBridge.find("") - findShowing = false - } - with(density) { - EmbeddedFindBar( - bridge = findBridge, - onClose = { findShowing = false }, - modifier = - Modifier - .absoluteOffset( - (bounds.left - layerOrigin.x).toDp(), - (bounds.top - layerOrigin.y).toDp(), - ).size(bounds.width.toDp(), bounds.height.toDp()), + // Find in page: opened from the pill's Find tile. + if (findShowing && findBridge != null) { + BackHandler { closeFind() } + val result by findBridge.findResult + FindInPagePill( + query = findQuery, + onQueryChange = { + findQuery = it + findBridge.find(it) + }, + active = result?.active ?: 0, + total = result?.total, + onNext = findBridge::findNext, + onClose = ::closeFind, + modifier = Modifier.align(Alignment.BottomCenter), ) } - } - // Bottom console panel: opened via the "Console" row in the top pull-down sheet. - if (consoleShowing && consoleBridge != null) { - with(density) { - BottomConsoleSheet( - logs = consoleBridge.consoleLogs, - expanded = consoleExpanded, - onExpandedChange = { consoleExpanded = it }, + // The developer console: opened from the pill's console row. + if (consoleShowing && consoleLogs != null) { + ConsoleSheet( + lines = consoleLogs, + onCopy = { lines -> BrowserWebTools.copyText(context, "console", lines.joinToString("\n", transform = ::formatConsoleLine)) }, onClear = { consoleBridge.clearConsoleLogs() }, - modifier = - Modifier - .absoluteOffset( - (bounds.left - layerOrigin.x).toDp(), - (bounds.top - layerOrigin.y).toDp(), - ).size(bounds.width.toDp(), bounds.height.toDp()), + onCopyLine = { BrowserWebTools.copyText(context, "console", formatConsoleLine(it)) }, + onClose = { consoleShowing = false }, + modifier = Modifier.align(Alignment.BottomCenter), ) } } @@ -1001,3 +1018,16 @@ private fun SelectionToolbarItem( }.padding(horizontal = 12.dp, vertical = 10.dp), ) } + +/** One console line as plain text, for copying. */ +private fun formatConsoleLine(line: ConsoleLine): String = + buildString { + append(line.level.name).append(": ").append(line.message) + if (line.source.isNotBlank()) { + append(" (") + .append(line.source) + .append(':') + .append(line.line) + .append(')') + } + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/TopControlSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/TopControlSheet.kt deleted file mode 100644 index 892cdba6d3..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/TopControlSheet.kt +++ /dev/null @@ -1,459 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.embed - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.draggable -import androidx.compose.foundation.gestures.rememberDraggableState -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.browser.BrowserChrome -import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.Action -import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.SectionKind -import com.vitorpamplona.amethyst.commons.icons.symbols.Icon -import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol -import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols -import com.vitorpamplona.amethyst.napplethost.BrowserChromeLabels -import com.vitorpamplona.amethyst.ui.stringRes -import androidx.compose.material3.Icon as Material3Icon -import com.vitorpamplona.amethyst.commons.R as CommonsR - -/** - * A top **pull-down pill** for a running app surface. Collapsed it's just a small grabber centered at the - * very top edge — out of the corner where a site puts its own avatar/menu. Pulled down (or tapped) it shows a - * Chrome-PWA-style app menu: the page title with its origin and connection badge (tap for page info), the - * icon row (back · forward · reload/stop · star · share), then the menu rows and the Privacy and Developer - * groups. The page can't draw over it (the surface is z-ordered below this layer). - * - * The layout comes from [BrowserChrome] and the icons/labels from [BrowserChromeLabels] — both shared with - * the full-screen browser's native sheet — so an action looks and sits the same in either. - * - * @param onConsole When non-null, CONSOLE toggles the log panel through it ([consoleCount] messages so far). - * @param onFind When non-null, FIND_IN_PAGE opens the find bar through it. - */ -@Composable -fun TopControlSheet( - chrome: EmbeddedTabChrome, - expanded: Boolean, - onExpandedChange: (Boolean) -> Unit, - modifier: Modifier = Modifier, - consoleCount: Int = 0, - consoleShowing: Boolean = false, - onConsole: (() -> Unit)? = null, - onFind: (() -> Unit)? = null, -) { - val state = remember(chrome.state, onConsole) { if (onConsole == null) chrome.state.copy(hasConsole = false) else chrome.state } - var editingAddress by remember(expanded) { mutableStateOf(false) } - - fun act(action: Action) { - when (action) { - Action.CONSOLE -> onConsole?.invoke() - Action.FIND_IN_PAGE -> onFind?.invoke() - else -> chrome.onAction(action) - } - } - - Column( - modifier = modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AnimatedVisibility( - visible = expanded, - enter = expandVertically() + fadeIn(), - exit = shrinkVertically() + fadeOut(), - ) { - Surface( - color = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface, - // No tonal elevation. Material 3 only recolors a Surface whose color is EXACTLY - // colorScheme.surface — it swaps in surfaceColorAtElevation(), which blends surfaceTint - // (= primary, Amethyst's purple) over the surface. On the light theme that near-white + - // purple mix reads as a pink/lilac cast instead of the plain background the sheet should - // have. Keep the drop shadow (shadowElevation) to lift the sheet off the page below it. - tonalElevation = 0.dp, - shadowElevation = 6.dp, - shape = RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp), - ) { - Column(Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 6.dp)) { - SheetHeader( - chrome = chrome, - state = state, - editingAddress = editingAddress, - onOriginTap = { - onExpandedChange(false) - chrome.onOriginTap() - }, - onCopy = { chrome.onAction(Action.COPY_LINK) }, - onNavigate = { text -> - editingAddress = false - onExpandedChange(false) - chrome.onNavigate(text) - }, - ) - IconActionRow(state, chrome.isFavorite) { action -> - onExpandedChange(false) - act(action) - } - HorizontalDivider() - val maxHeight = (LocalConfiguration.current.screenHeightDp * 0.55f).dp - Column(Modifier.heightIn(max = maxHeight).verticalScroll(rememberScrollState())) { - BrowserChrome.sections(state).forEachIndexed { index, section -> - if (index > 0) HorizontalDivider(Modifier.padding(vertical = 2.dp)) - sectionTitle(section.kind)?.let { - Text( - it.uppercase(), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(start = 8.dp, top = 10.dp, bottom = 2.dp), - ) - } - section.actions.forEach { action -> - when { - action == Action.TEXT_SIZE -> TextSizeRow(chrome.textZoom, chrome.onTextZoom) - BrowserChromeLabels.isToggle(action) -> - SheetSwitchItem( - action = action, - label = rowLabel(action, state, chrome.isFavorite, consoleCount), - checked = - when (action) { - Action.TOR -> state.torOn == true - Action.DESKTOP_SITE -> chrome.desktopSite - else -> consoleShowing - }, - ) { - onExpandedChange(false) - act(action) - } - else -> - SheetItem(action, rowLabel(action, state, chrome.isFavorite, consoleCount)) { - if (action == Action.EDIT_ADDRESS) { - editingAddress = true - } else { - onExpandedChange(false) - act(action) - } - } - } - } - } - } - } - } - } - - // The grabber: a small rounded bar centered at the top edge. It is the ONLY touch target the sheet - // draws — the rest of the top strip stays transparent so page taps pass straight through to the - // surface below. Pull down to open, up to close; tapping toggles. - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = - Modifier - .clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)) - .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.6f)) - .clickable { onExpandedChange(!expanded) } - .draggable( - orientation = Orientation.Vertical, - state = - rememberDraggableState { delta -> - if (delta > 1f) { - onExpandedChange(true) - } else if (delta < -1f) { - onExpandedChange(false) - } - }, - ).padding(horizontal = 16.dp, vertical = 7.dp), - ) { - Spacer( - Modifier - .width(36.dp) - .height(5.dp) - .clip(RoundedCornerShape(50)) - .background(MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)), - ) - } - } -} - -@Composable -private fun sectionTitle(kind: SectionKind): String? = - when (kind) { - SectionKind.PAGE -> null - SectionKind.PRIVACY -> stringRes(CommonsR.string.browser_section_privacy) - SectionKind.DEVELOPER -> stringRes(CommonsR.string.browser_section_developer) - } - -@Composable -private fun rowLabel( - action: Action, - state: BrowserChrome.State, - isFavorite: Boolean, - consoleCount: Int, -): String = - when { - action == Action.BACK_TO_APP -> stringRes(CommonsR.string.browser_action_back_to_app, BrowserChrome.displayHost(state.startUrl)) - action == Action.CONSOLE && consoleCount > 0 -> stringRes(CommonsR.string.browser_console_title, consoleCount) - else -> stringRes(BrowserChromeLabels.labelFor(action, isFavorite = isFavorite, torOn = state.torOn == true)) - } - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun SheetHeader( - chrome: EmbeddedTabChrome, - state: BrowserChrome.State, - editingAddress: Boolean, - onOriginTap: () -> Unit, - onCopy: () -> Unit, - onNavigate: (String) -> Unit, -) { - val security = BrowserChrome.security(state) - Row(Modifier.padding(horizontal = 8.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically) { - SymbolOrDrawable( - BrowserChromeLabels.securitySymbol(security), - BrowserChromeLabels.securityDrawable(security), - tint = if (state.isSandbox) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, - size = 20, - ) - Spacer(Modifier.width(12.dp)) - if (editingAddress) { - AddressField(state.url, onNavigate, Modifier.weight(1f)) - } else { - Column( - Modifier - .weight(1f) - .clip(RoundedCornerShape(8.dp)) - .combinedClickable(onClick = onOriginTap, onLongClick = if (state.isSandbox) null else onCopy), - ) { - Text(chrome.title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis) - val securityLabel = stringRes(BrowserChromeLabels.securityLabel(security)) - Text( - if (state.isSandbox) securityLabel else BrowserChrome.displayHost(state.url) + " · " + securityLabel, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - } -} - -/** The rarely used editable address, swapped in for the origin chip by "Edit address". */ -@Composable -private fun AddressField( - url: String, - onNavigate: (String) -> Unit, - modifier: Modifier = Modifier, -) { - var field by remember { mutableStateOf(TextFieldValue(url, TextRange(0, url.length))) } - val focus = remember { FocusRequester() } - LaunchedEffect(Unit) { focus.requestFocus() } - TextField( - value = field, - onValueChange = { field = it }, - modifier = modifier.focusRequester(focus), - singleLine = true, - placeholder = { Text(stringRes(CommonsR.string.browser_address_hint)) }, - keyboardOptions = - KeyboardOptions( - capitalization = KeyboardCapitalization.None, - autoCorrectEnabled = false, - keyboardType = KeyboardType.Uri, - imeAction = ImeAction.Go, - ), - keyboardActions = - KeyboardActions(onGo = { - field.text - .trim() - .takeIf { it.isNotEmpty() } - ?.let(onNavigate) - }), - colors = - TextFieldDefaults.colors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), - ) -} - -@Composable -private fun IconActionRow( - state: BrowserChrome.State, - isFavorite: Boolean, - onAction: (Action) -> Unit, -) { - Row(Modifier.fillMaxWidth().padding(vertical = 2.dp), horizontalArrangement = Arrangement.SpaceEvenly) { - BrowserChrome.iconRow(state).forEach { action -> - val enabled = BrowserChrome.isEnabled(state, action) - val favorite = action == Action.FAVORITE && isFavorite - IconButton(onClick = { onAction(action) }, enabled = enabled) { - Icon( - BrowserChromeLabels.symbolFor(action)!!, - contentDescription = stringRes(BrowserChromeLabels.labelFor(action, isFavorite = isFavorite)), - modifier = Modifier.size(24.dp), - tint = - when { - !enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.35f) - favorite -> MaterialTheme.colorScheme.primary - else -> MaterialTheme.colorScheme.onSurface - }, - filled = favorite, - ) - } - } - } -} - -@Composable -private fun SymbolOrDrawable( - symbol: MaterialSymbol?, - drawable: Int?, - tint: Color, - size: Int, -) { - if (drawable != null) { - Material3Icon(painterResource(drawable), contentDescription = null, modifier = Modifier.size(size.dp), tint = tint) - } else if (symbol != null) { - Icon(symbol, contentDescription = null, modifier = Modifier.size(size.dp), tint = tint) - } -} - -@Composable -private fun SheetItem( - action: Action, - label: String, - onClick: () -> Unit, -) { - Row( - Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)) - .clickable(onClick = onClick) - .padding(horizontal = 8.dp, vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - SymbolOrDrawable(BrowserChromeLabels.symbolFor(action), BrowserChromeLabels.drawableFor(action), MaterialTheme.colorScheme.onSurfaceVariant, 22) - Spacer(Modifier.width(14.dp)) - Text(label, style = MaterialTheme.typography.bodyLarge) - } -} - -@Composable -private fun SheetSwitchItem( - action: Action, - label: String, - checked: Boolean, - onToggle: () -> Unit, -) { - Row( - Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)) - .clickable(onClick = onToggle) - // Same row rhythm as [SheetItem] so every entry lines up; the Switch is taller but the - // padding (the inter-item spacing) is identical. - .padding(horizontal = 8.dp, vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - SymbolOrDrawable(BrowserChromeLabels.symbolFor(action), BrowserChromeLabels.drawableFor(action), MaterialTheme.colorScheme.onSurfaceVariant, 22) - Spacer(Modifier.width(14.dp)) - Text(label, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f)) - Switch(checked = checked, onCheckedChange = { onToggle() }) - } -} - -@Composable -private fun TextSizeRow( - textZoom: Int, - onTextZoom: (Int) -> Unit, -) { - Row( - Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon(MaterialSymbols.FormatSize, contentDescription = null, modifier = Modifier.size(22.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant) - Spacer(Modifier.width(14.dp)) - Text(stringRes(CommonsR.string.browser_action_text_size), style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f)) - IconButton(onClick = { onTextZoom(BrowserChrome.stepTextZoom(textZoom, larger = false)) }) { - Icon(MaterialSymbols.Remove, contentDescription = stringRes(CommonsR.string.browser_action_text_smaller)) - } - Text( - stringRes(CommonsR.string.browser_action_text_size_value, textZoom), - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.widthIn(min = 44.dp), - ) - IconButton(onClick = { onTextZoom(BrowserChrome.stepTextZoom(textZoom, larger = true)) }) { - Icon(MaterialSymbols.Add, contentDescription = stringRes(CommonsR.string.browser_action_text_larger)) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt index 6a4a87eb83..a3b12d3d61 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt @@ -55,6 +55,8 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPillEvent +import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPillUi import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.navigation.favoriteIds @@ -174,21 +176,24 @@ private fun EmbeddedNostrAppTab( val chrome = remember(title, coordinate, isFavorite, controller) { EmbeddedTabChrome( - title = title.ifBlank { coordinate }, - state = - BrowserChrome.State( - surface = BrowserChrome.Surface.NAPPLET, - presentation = BrowserChrome.Presentation.EMBEDDED, - url = "", - startUrl = "", - hasAccessInfo = true, - // The embedded nsite/napplet host has no find or text-size plumbing (yet). - hasFind = false, - hasTextSize = false, + ui = + BrowserPillUi( + title = title.ifBlank { coordinate }, + chrome = + BrowserChrome.State( + surface = BrowserChrome.Surface.NAPPLET, + presentation = BrowserChrome.Presentation.EMBEDDED, + url = "", + startUrl = "", + hasAccessInfo = true, + // The embedded nsite/napplet host has no find or text-size plumbing (yet). + hasFind = false, + hasTextSize = false, + ), + isFavorite = isFavorite, ), - isFavorite = isFavorite, - onAction = { action -> - when (action) { + onEvent = { event -> + when ((event as? BrowserPillEvent.Action)?.action) { BrowserChrome.Action.RELOAD -> controller.reload() BrowserChrome.Action.OPEN_FULL_SCREEN -> FavoriteAppLauncher.launch(context, FavoriteApp.NostrApp(coordinate, title, System.currentTimeMillis()), appStillLoadingStr) @@ -205,7 +210,6 @@ private fun EmbeddedNostrAppTab( else -> Unit } }, - onOriginTap = { showAccess = true }, ) } // Publish the top-sheet controls to the tab layer (drawn over the z-below surface). In a SideEffect diff --git a/commons/src/androidMain/res/values/strings.xml b/commons/src/androidMain/res/values/strings.xml index 6e673d6fda..2d9ffabb12 100644 --- a/commons/src/androidMain/res/values/strings.xml +++ b/commons/src/androidMain/res/values/strings.xml @@ -9,79 +9,22 @@ <string name="browser_file_chooser_title">Choose a file</string> <!-- Shown when the device has no app that can hand a file back to the page. --> <string name="browser_file_chooser_unavailable">No app available to pick a file</string> - <!-- Browser top pill (shared by the embedded tab and the full-screen browser) --> - <string name="browser_action_back">Back</string> - <string name="browser_action_forward">Forward</string> - <string name="browser_action_reload">Reload</string> - <string name="browser_action_stop">Stop loading</string> - <string name="browser_action_favorite_add">Add to favorites</string> - <string name="browser_action_favorite_remove">Remove from favorites</string> - <string name="browser_action_share">Share…</string> - <string name="browser_action_back_to_app">Back to %1$s</string> - <string name="browser_action_copy_link">Copy link</string> - <string name="browser_action_edit_address">Edit address</string> - <string name="browser_action_find_in_page">Find in page</string> - <string name="browser_action_text_size">Text size</string> - <string name="browser_action_text_size_value">%1$d%%</string> - <string name="browser_action_text_smaller">Smaller text</string> - <string name="browser_action_text_larger">Larger text</string> - <string name="browser_action_desktop_site">Desktop site</string> - <string name="browser_action_add_to_home">Add to Home screen</string> - <string name="browser_action_open_in_browser_app">Open in another browser</string> - <string name="browser_action_open_full_screen">Open full screen</string> - <string name="browser_action_tor_on">Loads over Tor</string> - <string name="browser_action_tor_off">Loads over the open web</string> - <string name="browser_action_access_info">What it can access</string> - <string name="browser_action_site_settings">Site settings & permissions</string> - <string name="browser_action_close">Close</string> - <string name="browser_section_privacy">Privacy</string> - <string name="browser_section_developer">Developer</string> - <string name="browser_security_tor">Onion-routed over Tor</string> - <string name="browser_security_https">Secure connection</string> - <string name="browser_security_http">Not secure</string> - <string name="browser_security_sandbox">Sandboxed app</string> + <!-- Browser actions --> <string name="browser_link_copied">Link copied</string> <string name="browser_no_other_browser">No other browser installed</string> - <!-- Find in page --> - <string name="browser_find_hint">Find in page</string> - <string name="browser_find_count">%1$d/%2$d</string> - <string name="browser_find_previous">Previous match</string> - <string name="browser_find_next">Next match</string> - <string name="browser_find_close">Close find in page</string> - - <!-- Page-originated JavaScript dialogs (alert / confirm / prompt / beforeunload) --> - <string name="browser_js_dialog_title">%1$s says</string> - <string name="browser_js_dialog_title_generic">This page says</string> - <string name="browser_js_dialog_block">Block dialogs from this page</string> - <string name="browser_js_leave_title">Leave site?</string> - <string name="browser_js_leave_message">Changes you made may not be saved.</string> - <string name="browser_js_leave">Leave</string> - <!-- Downloads and sharing --> <string name="browser_download_started">Downloading %1$s…</string> <string name="browser_download_saved">Saved %1$s to Downloads</string> <string name="browser_download_failed">Couldn\'t download %1$s</string> <string name="browser_share_chooser">Share</string> - <!-- Page info --> - <string name="browser_page_info_title">Page info</string> - <string name="browser_page_info_https">Your connection to this site is encrypted.</string> - <string name="browser_page_info_http">Your connection to this site is not encrypted. Don\'t enter passwords or payment details.</string> - <string name="browser_page_info_tor">This site loads over Tor. The site can\'t see your IP address.</string> - <string name="browser_page_info_open_web">This site loads over the open web. The site can see your IP address.</string> - <string name="browser_page_info_certificate">Certificate issued to %1$s by %2$s, valid until %3$s.</string> - <string name="browser_page_info_permissions">Permissions</string> - <string name="browser_page_info_clear_data">Clear site data</string> <string name="browser_site_data_cleared">Site data cleared</string> <!-- Site permissions (camera / microphone / location) --> - <string name="browser_permission_title">%1$s wants to</string> <string name="browser_permission_camera">Use your camera</string> <string name="browser_permission_microphone">Use your microphone</string> <string name="browser_permission_location">Know your approximate location</string> - <string name="browser_permission_allow">Allow</string> - <string name="browser_permission_block">Block</string> <string name="browser_permission_system_denied">Amethyst doesn\'t have Android\'s permission for this. Allow it in system settings.</string> <string name="browser_permission_ask">Ask</string> <string name="browser_permission_allowed">Allowed</string> diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/AddressEditor.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/AddressEditor.kt index f46a97d35e..3175737aec 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/AddressEditor.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/AddressEditor.kt @@ -76,24 +76,29 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes /** * The address as a first-class input, opened from the origin field. A 56dp pill field with the URL * pre-selected, a leading search/security icon, and trailing clear + a filled Go; below it Paste and go - * (when the clipboard holds a URL) and the omnibox suggestions, each with a ↖ that fills its address in - * without leaving. [onCancel] folds it back into the origin field. + * (when the clipboard holds a URL) and the omnibox suggestions for what's typed ([suggestionsFor]), each + * with a ↖ that fills its address in without leaving. [onCancel] folds it back into the origin field. */ @Composable fun AddressEditor( initialUrl: String, security: BrowserChrome.Security, - suggestions: List<AddressSuggestion>, + suggestionsFor: (String) -> List<AddressSuggestion>, clipboardUrl: String?, onGo: (String) -> Unit, onCancel: () -> Unit, modifier: Modifier = Modifier, autoFocus: Boolean = true, + onPasteAndGo: (() -> Unit)? = null, ) { var field by remember { mutableStateOf(TextFieldValue(initialUrl, TextRange(0, initialUrl.length))) } val focus = remember { FocusRequester() } if (autoFocus) LaunchedEffect(Unit) { runCatching { focus.requestFocus() } } + // Ranked against what the user typed; the untouched page URL counts as nothing typed yet. + val typed = if (field.text == initialUrl) "" else field.text + val suggestions = remember(typed) { suggestionsFor(typed) } + fun go(text: String = field.text) { text.trim().takeIf { it.isNotEmpty() }?.let(onGo) } @@ -153,10 +158,13 @@ fun AddressEditor( } } - if (clipboardUrl != null && clipboardUrl != field.text) { + // [clipboardUrl] when the host may read the clipboard up front; otherwise [onPasteAndGo] reads it only + // on tap (Android announces every clipboard read, so the offer can't peek at the contents). + if ((clipboardUrl != null && clipboardUrl != field.text) || onPasteAndGo != null) { + val pasteLabel = stringRes(Res.string.browser_pill_paste_go) AssistChip( - onClick = { go(clipboardUrl) }, - label = { Text(stringRes(Res.string.browser_pill_paste_go) + " · " + BrowserChrome.displayHost(clipboardUrl), maxLines = 1, overflow = TextOverflow.Ellipsis) }, + onClick = { if (clipboardUrl != null) go(clipboardUrl) else onPasteAndGo?.invoke() }, + label = { Text(if (clipboardUrl != null) pasteLabel + " · " + BrowserChrome.displayHost(clipboardUrl) else pasteLabel, maxLines = 1, overflow = TextOverflow.Ellipsis) }, leadingIcon = { Icon(MaterialSymbols.ContentPasteGo, contentDescription = null, modifier = Modifier.size(AssistChipDefaults.IconSize)) }, modifier = Modifier.padding(start = 48.dp), ) diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserChromeTheme.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserChromeTheme.kt new file mode 100644 index 0000000000..b7875a0a48 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserChromeTheme.kt @@ -0,0 +1,35 @@ +/* + * 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.commons.browser.ui.pill + +import androidx.compose.runtime.Composable +import com.vitorpamplona.amethyst.commons.ui.theme.AmethystPreviewTheme + +/** + * The Material theme for browser chrome drawn outside the main app's composition — the full-screen + * browser and nsite/napplet windows in the keyless `:napplet` process, which has no access to the user's + * theme preferences beyond light/dark. Uses Amethyst's default palette, typography and shapes. + */ +@Composable +fun BrowserChromeTheme( + dark: Boolean, + content: @Composable () -> Unit, +) = AmethystPreviewTheme(dark = dark, content = content) diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPill.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPill.kt index e7bd6ca2a3..3c261ee149 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPill.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPill.kt @@ -109,10 +109,11 @@ fun BrowserPill( onEvent: (BrowserPillEvent) -> Unit, modifier: Modifier = Modifier, showClose: Boolean = false, - suggestions: List<AddressSuggestion> = emptyList(), + suggestionsFor: (String) -> List<AddressSuggestion> = { emptyList() }, clipboardUrl: String? = null, initiallyEditing: Boolean = false, initiallyTextSizeOpen: Boolean = false, + onPasteAndGo: (() -> Unit)? = null, ) { Column(modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { AnimatedVisibility( @@ -128,10 +129,11 @@ fun BrowserPill( onEvent(event) }, showClose = showClose, - suggestions = suggestions, + suggestionsFor = suggestionsFor, clipboardUrl = clipboardUrl, initiallyEditing = initiallyEditing, initiallyTextSizeOpen = initiallyTextSizeOpen, + onPasteAndGo = onPasteAndGo, ) } PillHandle(ui, expanded, onExpandedChange) @@ -213,10 +215,11 @@ fun BrowserPillSheet( onEvent: (BrowserPillEvent) -> Unit, modifier: Modifier = Modifier, showClose: Boolean = false, - suggestions: List<AddressSuggestion> = emptyList(), + suggestionsFor: (String) -> List<AddressSuggestion> = { emptyList() }, clipboardUrl: String? = null, initiallyEditing: Boolean = false, initiallyTextSizeOpen: Boolean = false, + onPasteAndGo: (() -> Unit)? = null, ) { var editing by rememberSaveable { mutableStateOf(initiallyEditing) } var textSizeOpen by rememberSaveable { mutableStateOf(initiallyTextSizeOpen) } @@ -244,10 +247,11 @@ fun BrowserPillSheet( AddressEditor( initialUrl = ui.chrome.url, security = ui.security, - suggestions = suggestions, + suggestionsFor = suggestionsFor, clipboardUrl = clipboardUrl, onGo = { onEvent(BrowserPillEvent.Navigate(it)) }, onCancel = { editing = false }, + onPasteAndGo = onPasteAndGo, ) } else { PillHeader(ui, showClose, onClose = { onEvent(BrowserPillEvent.Close) }) @@ -255,6 +259,7 @@ fun BrowserPillSheet( ui = ui, onEdit = if (Action.EDIT_ADDRESS in page) ({ editing = true }) else null, onLongPress = if (!ui.chrome.isSandbox) ({ onEvent(BrowserPillEvent.CopyOrigin) }) else null, + onSecurityTap = if (!ui.chrome.isSandbox) ({ onEvent(BrowserPillEvent.PageInfo) }) else null, ) if (Action.BACK_TO_APP in page) { OutOfScopeBanner(homeHost = BrowserChrome.displayHost(ui.chrome.startUrl)) { diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillModel.kt index b9c78e7e1f..1d3ea5b87a 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillModel.kt @@ -67,6 +67,9 @@ sealed interface BrowserPillEvent { /** The origin field was long-pressed (copy link). */ data object CopyOrigin : BrowserPillEvent + /** The security badge at the start of the origin field was tapped: show page info. */ + data object PageInfo : BrowserPillEvent + data object Close : BrowserPillEvent } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillPreviews.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillPreviews.kt index fce006a7e9..ace3a1cf43 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillPreviews.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillPreviews.kt @@ -148,7 +148,7 @@ fun BrowserPillAddressEditorPreview() { expanded = true, onExpandedChange = {}, onEvent = {}, - suggestions = BrowserPillSamples.suggestions, + suggestionsFor = { BrowserPillSamples.suggestions }, clipboardUrl = "https://njump.me/npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z", initiallyEditing = true, ) diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/ConsoleSheet.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/ConsoleSheet.kt index 28de782946..c2f25cb6cc 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/ConsoleSheet.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/ConsoleSheet.kt @@ -65,6 +65,7 @@ import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.browser_pill_close import com.vitorpamplona.amethyst.commons.resources.browser_pill_console import com.vitorpamplona.amethyst.commons.resources.browser_pill_console_all import com.vitorpamplona.amethyst.commons.resources.browser_pill_console_clear @@ -91,6 +92,7 @@ fun ConsoleSheet( modifier: Modifier = Modifier, maxHeight: Dp = 320.dp, initialFilter: ConsoleFilter = ConsoleFilter.ALL, + onClose: (() -> Unit)? = null, ) { var filter by remember { mutableStateOf(initialFilter) } val errors = lines.count { it.level == ConsoleLine.Level.ERROR } @@ -127,6 +129,11 @@ fun ConsoleSheet( IconButton(onClick = onClear, enabled = lines.isNotEmpty()) { Icon(MaterialSymbols.Delete, contentDescription = stringRes(Res.string.browser_pill_console_clear), modifier = Modifier.size(20.dp)) } + if (onClose != null) { + IconButton(onClick = onClose) { + Icon(MaterialSymbols.Close, contentDescription = stringRes(Res.string.browser_pill_close), modifier = Modifier.size(20.dp)) + } + } } Row( Modifier.horizontalScroll(rememberScrollState()).padding(horizontal = 16.dp), diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/PillComponents.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/PillComponents.kt index 1dc373369b..226af88eb5 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/PillComponents.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/PillComponents.kt @@ -123,6 +123,7 @@ fun OriginField( onEdit: (() -> Unit)?, onLongPress: (() -> Unit)?, modifier: Modifier = Modifier, + onSecurityTap: (() -> Unit)? = null, ) { val security = ui.security Row( @@ -136,11 +137,17 @@ fun OriginField( role = Role.Button, onClick = { onEdit?.invoke() }, onLongClick = onLongPress, - ).padding(start = 16.dp, end = 6.dp), + ).padding(start = if (onSecurityTap != null) 4.dp else 16.dp, end = 6.dp), verticalAlignment = Alignment.CenterVertically, ) { - SecurityIcon(security, size = 18.dp) - Spacer(Modifier.width(10.dp)) + // Chrome's page-info entry point: the connection badge itself. + if (onSecurityTap != null) { + IconButton(onClick = onSecurityTap) { SecurityIcon(security, size = 20.dp) } + Spacer(Modifier.width(2.dp)) + } else { + SecurityIcon(security, size = 18.dp) + Spacer(Modifier.width(10.dp)) + } Column(Modifier.weight(1f).padding(vertical = 6.dp)) { Text( if (ui.chrome.isSandbox) stringRes(securityLabel(security)) else ui.host, diff --git a/nappletHost/build.gradle.kts b/nappletHost/build.gradle.kts index f10b0653c1..cfbaf52802 100644 --- a/nappletHost/build.gradle.kts +++ b/nappletHost/build.gradle.kts @@ -2,6 +2,8 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.androidLibrary) + // The full-screen browser / napplet windows draw the shared Compose browser chrome (commonsUI). + alias(libs.plugins.jetbrainsComposeCompiler) } android { @@ -43,6 +45,11 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.activity) + implementation(libs.jetbrains.compose.ui) + implementation(libs.jetbrains.compose.foundation) + implementation(libs.jetbrains.compose.runtime) + implementation(libs.jetbrains.compose.material3) + implementation(libs.jetbrains.compose.components.resources) implementation(libs.androidx.webkit) implementation(libs.okhttp) diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserChromeHost.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserChromeHost.kt new file mode 100644 index 0000000000..5041339b99 --- /dev/null +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserChromeHost.kt @@ -0,0 +1,388 @@ +/* + * 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.napplethost + +import android.view.Gravity +import android.view.ViewGroup +import android.webkit.ConsoleMessage +import android.widget.FrameLayout +import androidx.activity.ComponentActivity +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission +import com.vitorpamplona.amethyst.commons.browser.ui.pill.AddressSuggestion +import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserChromeTheme +import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPill +import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPillEvent +import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPillUi +import com.vitorpamplona.amethyst.commons.browser.ui.pill.CertificateInfo +import com.vitorpamplona.amethyst.commons.browser.ui.pill.ConsoleLine +import com.vitorpamplona.amethyst.commons.browser.ui.pill.ConsoleSheet +import com.vitorpamplona.amethyst.commons.browser.ui.pill.FindInPagePill +import com.vitorpamplona.amethyst.commons.browser.ui.pill.PageDialogCard +import com.vitorpamplona.amethyst.commons.browser.ui.pill.PageDialogType +import com.vitorpamplona.amethyst.commons.browser.ui.pill.PageInfoSheet +import com.vitorpamplona.amethyst.commons.browser.ui.pill.PermissionPromptCard + +/** + * The browser chrome of a full-screen `:napplet` window (the web browser and the nsite/napplet host), drawn + * with the shared Compose components from `commonsUI` — the same pill, find pill, console sheet and page + * cards the embedded tabs use, so both surfaces are pixel-identical. + * + * Owns the chrome's Compose state and two [ComposeView]s laid over the page: the pill at the top (grown to + * the full window while open, so a tap outside closes it) and find / console at the bottom. Page dialogs, + * permission prompts and page info open as Compose dialogs. The window only feeds state in ([ui], console + * lines, find results) and handles what comes out through [Listener]. + */ +class BrowserChromeHost( + private val activity: ComponentActivity, + private val dark: Boolean, + initial: BrowserPillUi, + private val listener: Listener, + private val showClose: Boolean = true, + private val suggestionsFor: (String) -> List<AddressSuggestion> = { emptyList() }, +) { + /** What the chrome asks its window to do. */ + interface Listener { + /** Everything from the pill except find and the console, which the host runs itself. */ + fun onPillEvent(event: BrowserPillEvent) + + fun onFind(query: String) {} + + fun onFindNext(forward: Boolean) {} + + fun onFindClosed() {} + + fun onPermissionChange( + permission: BrowserSitePermission, + decision: BrowserSitePermission.Decision, + ) {} + + fun onClearSiteData() {} + + /** The pill, find or the console opened or closed (the window may need to route back). */ + fun onPanelsChanged() {} + } + + /** A page's JS dialog waiting for an answer. */ + class PendingDialog( + val type: PageDialogType, + val host: String?, + val security: BrowserChrome.Security, + val message: String, + val defaultValue: String, + val offerBlock: Boolean, + val answer: (confirmed: Boolean, text: String?, block: Boolean) -> Unit, + ) + + /** A site permission request waiting for an answer: allow or not, and whether to remember the choice. */ + class PendingPermission( + val host: String, + val security: BrowserChrome.Security, + val permissions: Set<BrowserSitePermission>, + val answer: (allow: Boolean, remember: Boolean) -> Unit, + ) + + var ui by mutableStateOf(initial) + var expanded by mutableStateOf(false) + private set + + var findOpen by mutableStateOf(false) + private set + private var findQuery by mutableStateOf("") + private var findActive by mutableStateOf(0) + private var findTotal by mutableStateOf<Int?>(null) + + var consoleShowing by mutableStateOf(false) + private set + val console = mutableStateListOf<ConsoleLine>() + + var dialog by mutableStateOf<PendingDialog?>(null) + var permissionPrompt by mutableStateOf<PendingPermission?>(null) + private var pageInfoOpen by mutableStateOf(false) + private var certificate by mutableStateOf<CertificateInfo?>(null) + + private var topView: ComposeView? = null + + /** Lays the chrome over [root], above the page. */ + fun attach(root: FrameLayout) { + val top = + ComposeView(activity).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { BrowserChromeTheme(dark) { TopChrome() } } + } + topView = top + root.addView(top, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP)) + val bottom = + ComposeView(activity).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { BrowserChromeTheme(dark) { BottomChrome() } } + } + root.addView(bottom, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM)) + } + + fun showPill(open: Boolean) { + if (expanded == open) return + expanded = open + // Open, the pill's view spans the window so a tap anywhere outside the sheet closes it; closed, it + // shrinks back to the grabber so every other touch reaches the page. + topView?.let { view -> + view.layoutParams = (view.layoutParams as FrameLayout.LayoutParams).apply { height = if (open) ViewGroup.LayoutParams.MATCH_PARENT else ViewGroup.LayoutParams.WRAP_CONTENT } + } + listener.onPanelsChanged() + } + + fun openFind() { + consoleShowing = false + findOpen = true + listener.onPanelsChanged() + } + + fun closeFind() { + if (!findOpen) return + findOpen = false + findQuery = "" + findTotal = null + listener.onFindClosed() + listener.onPanelsChanged() + } + + fun showConsole(show: Boolean) { + if (show) closeFind() + consoleShowing = show + listener.onPanelsChanged() + } + + fun setFindResult( + active: Int, + total: Int, + ) { + findActive = active + findTotal = total + } + + fun appendConsole(line: ConsoleLine) { + if (console.size >= MAX_CONSOLE_LINES) console.removeAt(0) + console.add(line) + } + + /** Page info for the page on screen, with its certificate when it has one. */ + fun showPageInfo(certificate: CertificateInfo?) { + this.certificate = certificate + pageInfoOpen = true + } + + /** Back closes, in order: the open pill, find, then nothing (the page's own history). */ + fun handleBack(): Boolean = + when { + expanded -> { + showPill(false) + true + } + findOpen -> { + closeFind() + true + } + else -> false + } + + val wantsBack: Boolean get() = expanded || findOpen + + private fun uiWithConsole(): BrowserPillUi = ui.copy(consoleShowing = consoleShowing, consoleErrors = console.count { it.level == ConsoleLine.Level.ERROR }) + + @Composable + private fun TopChrome() { + Box(Modifier.fillMaxSize()) { + if (expanded) { + Box( + Modifier + .fillMaxSize() + .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) { showPill(false) }, + ) + } + BrowserPill( + ui = uiWithConsole(), + expanded = expanded, + onExpandedChange = ::showPill, + onEvent = { event -> + when { + event is BrowserPillEvent.Action && event.action == BrowserChrome.Action.FIND_IN_PAGE -> openFind() + event is BrowserPillEvent.Action && event.action == BrowserChrome.Action.CONSOLE -> showConsole(!consoleShowing) + else -> listener.onPillEvent(event) + } + }, + showClose = showClose, + suggestionsFor = suggestionsFor, + onPasteAndGo = if (clipboardHasText()) ({ pasteAndGo() }) else null, + modifier = Modifier.align(Alignment.TopCenter), + ) + } + PageDialogs() + } + + @Composable + private fun BottomChrome() { + when { + findOpen -> + FindInPagePill( + query = findQuery, + onQueryChange = { + findQuery = it + if (it.isEmpty()) findTotal = null + listener.onFind(it) + }, + active = findActive, + total = findTotal, + onNext = listener::onFindNext, + onClose = ::closeFind, + ) + consoleShowing -> + ConsoleSheet( + lines = console, + onCopy = { lines -> copy(lines.joinToString("\n") { formatLine(it) }) }, + onClear = { console.clear() }, + onCopyLine = { copy(formatLine(it)) }, + onClose = { showConsole(false) }, + ) + } + } + + @Composable + private fun PageDialogs() { + dialog?.let { pending -> + Dialog(onDismissRequest = { + dialog = null + pending.answer(false, null, false) + }) { + PageDialogCard( + type = pending.type, + host = pending.host, + security = pending.security, + message = pending.message, + defaultValue = pending.defaultValue, + offerBlock = pending.offerBlock, + onResult = { confirmed, text, block -> + dialog = null + pending.answer(confirmed, text, block) + }, + ) + } + } + permissionPrompt?.let { pending -> + Dialog(onDismissRequest = { + permissionPrompt = null + pending.answer(false, false) + }) { + PermissionPromptCard( + host = pending.host, + security = pending.security, + permissions = pending.permissions, + onAllow = { + permissionPrompt = null + pending.answer(true, true) + }, + onAllowOnce = { + permissionPrompt = null + pending.answer(true, false) + }, + onDeny = { + permissionPrompt = null + pending.answer(false, true) + }, + ) + } + } + if (pageInfoOpen) { + Dialog(onDismissRequest = { pageInfoOpen = false }, properties = DialogProperties(usePlatformDefaultWidth = false)) { + Box(Modifier.fillMaxWidth().padding(16.dp), contentAlignment = Alignment.Center) { + PageInfoSheet( + ui = ui, + certificate = certificate, + onPermissionChange = { permission, decision -> + ui = ui.copy(sitePermissions = ui.sitePermissions + (permission to decision)) + listener.onPermissionChange(permission, decision) + }, + onClearSiteData = { + pageInfoOpen = false + listener.onClearSiteData() + }, + modifier = Modifier.widthIn(max = 560.dp), + ) + } + } + } + } + + private fun clipboardHasText(): Boolean = BrowserWebTools.clipboardHasText(activity) + + private fun pasteAndGo() { + val text = BrowserWebTools.clipboardText(activity) + showPill(false) + if (text != null) listener.onPillEvent(BrowserPillEvent.Navigate(text)) + } + + private fun copy(text: String) = BrowserWebTools.copyText(activity, "console", text) + + private fun formatLine(line: ConsoleLine): String = + buildString { + append(line.level.name).append(": ").append(line.message) + if (line.source.isNotBlank()) { + append(" (") + .append(line.source) + .append(':') + .append(line.line) + .append(')') + } + } + + companion object { + private const val MAX_CONSOLE_LINES = 500 + + /** Maps WebView's console level onto the chrome's. */ + fun levelOf(level: ConsoleMessage.MessageLevel): ConsoleLine.Level = + when (level) { + ConsoleMessage.MessageLevel.ERROR -> ConsoleLine.Level.ERROR + ConsoleMessage.MessageLevel.WARNING -> ConsoleLine.Level.WARNING + ConsoleMessage.MessageLevel.DEBUG -> ConsoleLine.Level.DEBUG + ConsoleMessage.MessageLevel.TIP -> ConsoleLine.Level.INFO + else -> ConsoleLine.Level.LOG + } + } +} diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserChromeLabels.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserChromeLabels.kt deleted file mode 100644 index 088aac02a3..0000000000 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserChromeLabels.kt +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.napplethost - -import android.content.Context -import android.graphics.Typeface -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import com.vitorpamplona.amethyst.commons.browser.BrowserChrome -import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.Action -import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.Security -import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol -import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols -import com.vitorpamplona.amethyst.commons.napplet.NappletWebContract -import com.vitorpamplona.quartz.utils.Log -import com.vitorpamplona.amethyst.commons.R as CommonsR - -/** - * How each [BrowserChrome.Action] looks: its Material Symbol (or, for Tor, the onion drawable) and its - * label. Shared by both renderers of the top pill — the Compose sheet over embedded tabs and the - * plain-View sheet in the full-screen browser — so an action has one icon and one name everywhere. - */ -object BrowserChromeLabels { - /** The glyph for [action], or null when it is drawn from [drawableFor] instead. */ - fun symbolFor(action: Action): MaterialSymbol? = - when (action) { - Action.BACK -> MaterialSymbols.AutoMirrored.ArrowBack - Action.FORWARD -> MaterialSymbols.AutoMirrored.ArrowForward - Action.RELOAD -> MaterialSymbols.Refresh - Action.STOP -> MaterialSymbols.Close - Action.FAVORITE -> MaterialSymbols.Star - Action.SHARE -> MaterialSymbols.Share - Action.BACK_TO_APP -> MaterialSymbols.Home - Action.COPY_LINK -> MaterialSymbols.ContentCopy - Action.EDIT_ADDRESS -> MaterialSymbols.Edit - Action.FIND_IN_PAGE -> MaterialSymbols.Search - Action.TEXT_SIZE -> MaterialSymbols.FormatSize - Action.DESKTOP_SITE -> MaterialSymbols.DesktopWindows - Action.ADD_TO_HOME_SCREEN -> MaterialSymbols.AddToHomeScreen - Action.OPEN_IN_BROWSER_APP -> MaterialSymbols.OpenInBrowser - Action.OPEN_FULL_SCREEN -> MaterialSymbols.OpenInFull - Action.TOR -> null - Action.ACCESS_INFO -> MaterialSymbols.Info - Action.SITE_SETTINGS -> MaterialSymbols.Tune - Action.CONSOLE -> MaterialSymbols.Code - } - - @DrawableRes - fun drawableFor(action: Action): Int? = if (action == Action.TOR) R.drawable.ic_tor else null - - /** The label for [action]; [isFavorite] and [torOn] pick the stateful wording. */ - @StringRes - fun labelFor( - action: Action, - isFavorite: Boolean = false, - torOn: Boolean = false, - ): Int = - when (action) { - Action.BACK -> CommonsR.string.browser_action_back - Action.FORWARD -> CommonsR.string.browser_action_forward - Action.RELOAD -> CommonsR.string.browser_action_reload - Action.STOP -> CommonsR.string.browser_action_stop - Action.FAVORITE -> if (isFavorite) CommonsR.string.browser_action_favorite_remove else CommonsR.string.browser_action_favorite_add - Action.SHARE -> CommonsR.string.browser_action_share - Action.BACK_TO_APP -> CommonsR.string.browser_action_back_to_app - Action.COPY_LINK -> CommonsR.string.browser_action_copy_link - Action.EDIT_ADDRESS -> CommonsR.string.browser_action_edit_address - Action.FIND_IN_PAGE -> CommonsR.string.browser_action_find_in_page - Action.TEXT_SIZE -> CommonsR.string.browser_action_text_size - Action.DESKTOP_SITE -> CommonsR.string.browser_action_desktop_site - Action.ADD_TO_HOME_SCREEN -> CommonsR.string.browser_action_add_to_home - Action.OPEN_IN_BROWSER_APP -> CommonsR.string.browser_action_open_in_browser_app - Action.OPEN_FULL_SCREEN -> CommonsR.string.browser_action_open_full_screen - Action.TOR -> if (torOn) CommonsR.string.browser_action_tor_on else CommonsR.string.browser_action_tor_off - Action.ACCESS_INFO -> CommonsR.string.browser_action_access_info - Action.SITE_SETTINGS -> CommonsR.string.browser_action_site_settings - Action.CONSOLE -> CommonsR.string.browser_console_title_short - } - - /** Actions whose row carries an on/off switch. */ - fun isToggle(action: Action): Boolean = action == Action.TOR || action == Action.DESKTOP_SITE || action == Action.CONSOLE - - /** Actions that keep the sheet open when used (the text-size stepper, the address editor). */ - fun keepsSheetOpen(action: Action): Boolean = action == Action.TEXT_SIZE || action == Action.EDIT_ADDRESS - - fun securitySymbol(security: Security): MaterialSymbol? = - when (security) { - Security.TOR -> null - Security.HTTPS -> MaterialSymbols.Lock - Security.HTTP -> MaterialSymbols.NoEncryption - Security.SANDBOX -> MaterialSymbols.Shield - } - - @DrawableRes - fun securityDrawable(security: Security): Int? = if (security == Security.TOR) R.drawable.ic_tor else null - - @StringRes - fun securityLabel(security: Security): Int = - when (security) { - Security.TOR -> CommonsR.string.browser_security_tor - Security.HTTPS -> CommonsR.string.browser_security_https - Security.HTTP -> CommonsR.string.browser_security_http - Security.SANDBOX -> CommonsR.string.browser_security_sandbox - } -} - -/** - * The Material Symbols font the Compose UI draws its icons from, loaded for plain Views. It ships inside - * `:commonsUI`'s compose resources (the same APK assets the napplet shim is read from), so the full-screen - * browser's controls use the very same glyphs as the embedded tab's. - */ -object BrowserGlyphs { - private const val FONT_PATH = "font/material_symbols_outlined.ttf" - - @Volatile private var cached: Typeface? = null - - @Volatile private var cachedFilled: Typeface? = null - - /** The same font with its FILL axis at 1 — a pinned page's solid star, as the Compose icon draws it. */ - fun filledTypeface(context: Context): Typeface = - cachedFilled ?: synchronized(this) { - cachedFilled ?: ( - runCatching { - Typeface - .Builder(context.assets, NappletWebContract.RESOURCE_ASSET_ROOT + FONT_PATH) - .setFontVariationSettings("'FILL' 1") - .build() - }.getOrNull() ?: typeface(context) - ).also { cachedFilled = it } - } - - fun typeface(context: Context): Typeface = - cached ?: synchronized(this) { - cached ?: runCatching { Typeface.createFromAsset(context.assets, NappletWebContract.RESOURCE_ASSET_ROOT + FONT_PATH) } - .onFailure { Log.w("BrowserGlyphs", "Material Symbols font missing from assets", it) } - .getOrDefault(Typeface.DEFAULT) - .also { cached = it } - } -} diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserFindBar.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserFindBar.kt deleted file mode 100644 index d96a24e118..0000000000 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserFindBar.kt +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.napplethost - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Color -import android.graphics.drawable.GradientDrawable -import android.text.Editable -import android.text.InputType -import android.text.TextWatcher -import android.util.TypedValue -import android.view.Gravity -import android.view.View -import android.view.inputmethod.EditorInfo -import android.view.inputmethod.InputMethodManager -import android.webkit.WebView -import android.widget.EditText -import android.widget.LinearLayout -import android.widget.TextView -import androidx.core.content.ContextCompat -import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol -import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols -import com.vitorpamplona.amethyst.commons.R as CommonsR - -/** - * The find-in-page bar for the full-screen surfaces, docked at the bottom edge (the top edge belongs to the - * pill's grabber, and the bottom keeps it off the page's own header). Drives [WebView.findAllAsync] / - * [WebView.findNext] on [webView] and shows "n/m". Hidden until [show]. - */ -@SuppressLint("ViewConstructor") -class BrowserFindBar( - context: Context, - private val webViewProvider: () -> WebView?, - private val onClosed: () -> Unit = {}, -) : LinearLayout(context) { - private val onSurface = color(android.R.attr.textColorPrimary) - private val dimmed = color(android.R.attr.textColorSecondary) - private val glyphs = BrowserGlyphs.typeface(context) - - private val field: EditText - private val count: TextView - - init { - orientation = HORIZONTAL - gravity = Gravity.CENTER_VERTICAL - visibility = View.GONE - elevation = dp(8).toFloat() - background = - GradientDrawable().apply { - cornerRadii = floatArrayOf(dp(16).toFloat(), dp(16).toFloat(), dp(16).toFloat(), dp(16).toFloat(), 0f, 0f, 0f, 0f) - setColor(color(android.R.attr.colorBackground)) - } - setPadding(dp(12), dp(4), dp(4), dp(4)) - field = - EditText(context).apply { - hint = context.getString(CommonsR.string.browser_find_hint) - setTextColor(onSurface) - setHintTextColor(dimmed) - background = null - isSingleLine = true - textSize = 15f - inputType = InputType.TYPE_CLASS_TEXT - imeOptions = EditorInfo.IME_ACTION_SEARCH - layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f) - addTextChangedListener( - object : TextWatcher { - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - after: Int, - ) = Unit - - override fun onTextChanged( - s: CharSequence?, - start: Int, - before: Int, - count: Int, - ) = Unit - - override fun afterTextChanged(s: Editable?) = search(s?.toString().orEmpty()) - }, - ) - setOnEditorActionListener { _, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_SEARCH) { - webViewProvider()?.findNext(true) - true - } else { - false - } - } - } - count = - TextView(context).apply { - setTextColor(dimmed) - textSize = 13f - setPadding(dp(8), 0, dp(4), 0) - } - addView(field) - addView(count) - addView(button(MaterialSymbols.KeyboardArrowUp, CommonsR.string.browser_find_previous) { webViewProvider()?.findNext(false) }) - addView(button(MaterialSymbols.KeyboardArrowDown, CommonsR.string.browser_find_next) { webViewProvider()?.findNext(true) }) - addView(button(MaterialSymbols.Close, CommonsR.string.browser_find_close) { hide() }) - } - - val isShowing: Boolean get() = visibility == View.VISIBLE - - fun show() { - webViewProvider()?.setFindListener { active, total, _ -> - count.text = - if (total > 0) { - context.getString(CommonsR.string.browser_find_count, active + 1, total) - } else if (field.text.isNullOrEmpty()) { - "" - } else { - "0/0" - } - } - visibility = View.VISIBLE - field.requestFocus() - field.selectAll() - context.getSystemService(InputMethodManager::class.java)?.showSoftInput(field, 0) - } - - fun hide() { - if (!isShowing) return - visibility = View.GONE - context.getSystemService(InputMethodManager::class.java)?.hideSoftInputFromWindow(field.windowToken, 0) - webViewProvider()?.apply { - clearMatches() - setFindListener(null) - } - count.text = "" - onClosed() - } - - private fun search(query: String) { - val wv = webViewProvider() ?: return - if (query.isEmpty()) { - wv.clearMatches() - count.text = "" - } else { - wv.findAllAsync(query) - } - } - - private fun button( - symbol: MaterialSymbol, - label: Int, - onClick: () -> Unit, - ): View = - TextView(context).apply { - text = symbol.glyph - typeface = glyphs - setTextColor(onSurface) - setTextSize(TypedValue.COMPLEX_UNIT_DIP, 22f) - gravity = Gravity.CENTER - minWidth = dp(44) - minHeight = dp(44) - contentDescription = context.getString(label) - tooltipText = contentDescription - isClickable = true - background = - TypedValue().let { tv -> - context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, tv, true) - ContextCompat.getDrawable(context, tv.resourceId) - } - setOnClickListener { onClick() } - } - - private fun color(attr: Int): Int { - val tv = TypedValue() - context.theme.resolveAttribute(attr, tv, true) - return if (tv.resourceId != 0) ContextCompat.getColor(context, tv.resourceId) else tv.data.takeIf { it != 0 } ?: Color.GRAY - } - - private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt() -} diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserJsDialogs.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserJsDialogs.kt deleted file mode 100644 index d2df5f3947..0000000000 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserJsDialogs.kt +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.napplethost - -import android.app.Activity -import android.app.AlertDialog -import android.text.InputType -import android.webkit.JsPromptResult -import android.webkit.JsResult -import android.widget.EditText -import android.widget.FrameLayout -import androidx.core.net.toUri -import com.vitorpamplona.amethyst.commons.R as CommonsR - -/** - * Shows a web page's `alert()` / `confirm()` / `prompt()` / `beforeunload` dialogs for the full-screen - * browser, the way Chrome does: titled with the page's origin ("example.com says") so a page can't pass - * its dialog off as Amethyst's own, and — from the second dialog on the same page — offering "Block - * dialogs from this page", so a script looping on alert() can't trap the user. The block lasts until the - * next main-frame navigation ([onMainFrameNavigation]). - * - * Every [JsResult] handed in is answered exactly once on every path (button, back, dismissal, a dialog - * already showing, the Activity going away): until it is, the page's JavaScript stays blocked. - */ -internal class BrowserJsDialogs( - private val activity: Activity, -) { - private var showing: AlertDialog? = null - private var shownOnThisPage = 0 - private var blocked = false - - /** A new main-frame load: the page that earned the block is gone. */ - fun onMainFrameNavigation() { - shownOnThisPage = 0 - blocked = false - } - - /** Closes a dialog still up (the Activity is being destroyed); its result is cancelled. */ - fun dismiss() { - showing?.dismiss() - showing = null - } - - fun alert( - url: String?, - message: String?, - result: JsResult, - ): Boolean = - show(result, onBlocked = { result.cancel() }) { answer -> - setTitle(titleFor(url)) - setMessage(message.orEmpty()) - setPositiveButton(android.R.string.ok) { _, _ -> answer { result.confirm() } } - } - - fun confirm( - url: String?, - message: String?, - result: JsResult, - ): Boolean = - show(result, onBlocked = { result.cancel() }) { answer -> - setTitle(titleFor(url)) - setMessage(message.orEmpty()) - setPositiveButton(android.R.string.ok) { _, _ -> answer { result.confirm() } } - setNegativeButton(android.R.string.cancel) { _, _ -> answer { result.cancel() } } - } - - fun prompt( - url: String?, - message: String?, - defaultValue: String?, - result: JsPromptResult, - ): Boolean { - val field = - EditText(activity).apply { - setText(defaultValue.orEmpty()) - setSelectAllOnFocus(true) - isSingleLine = true - inputType = InputType.TYPE_CLASS_TEXT - } - val container = - FrameLayout(activity).apply { - val pad = (20 * activity.resources.displayMetrics.density).toInt() - setPadding(pad, 0, pad, 0) - addView(field) - } - return show(result, onBlocked = { result.cancel() }) { answer -> - setTitle(titleFor(url)) - setMessage(message.orEmpty()) - setView(container) - setPositiveButton(android.R.string.ok) { _, _ -> answer { result.confirm(field.text?.toString().orEmpty()) } } - setNegativeButton(android.R.string.cancel) { _, _ -> answer { result.cancel() } } - } - } - - /** - * "Leave site?" — confirm lets the navigation proceed, cancel keeps the user on the page. The page's own - * message is ignored, as in every current browser (it was abused for scare text). A blocked page may - * no longer hold the user hostage, so blocked means "leave". - */ - fun beforeUnload(result: JsResult): Boolean = - show(result, onBlocked = { result.confirm() }) { answer -> - setTitle(CommonsR.string.browser_js_leave_title) - setMessage(CommonsR.string.browser_js_leave_message) - setPositiveButton(CommonsR.string.browser_js_leave) { _, _ -> answer { result.confirm() } } - setNegativeButton(android.R.string.cancel) { _, _ -> answer { result.cancel() } } - } - - /** - * Builds and shows one dialog. [build] wires the buttons through `answer`, which records that the - * result was settled so the dismiss listener doesn't cancel it a second time. Always returns true: the - * result is ours from here on. - */ - private fun show( - result: JsResult, - onBlocked: () -> Unit, - build: AlertDialog.Builder.(answer: (() -> Unit) -> Unit) -> Unit, - ): Boolean { - if (blocked) { - onBlocked() - return true - } - // One at a time, and never onto a window that is going away. - if (showing != null || activity.isFinishing || activity.isDestroyed) { - result.cancel() - return true - } - var settled = false - val answer: (() -> Unit) -> Unit = { action -> - if (!settled) { - settled = true - action() - } - } - shownOnThisPage++ - val builder = AlertDialog.Builder(activity).apply { build(answer) } - if (shownOnThisPage > 1) { - builder.setNeutralButton(CommonsR.string.browser_js_dialog_block) { _, _ -> - blocked = true - answer { onBlocked() } - } - } - val dialog = - builder - .setOnDismissListener { - showing = null - // Back, a tap outside, or teardown: no button answered, so the page gets a cancel. - answer { result.cancel() } - }.create() - showing = dialog - dialog.show() - return true - } - - private fun titleFor(url: String?): String { - val uri = url?.let { runCatching { it.toUri() }.getOrNull() } - val scheme = uri?.scheme?.lowercase() - val host = uri?.host?.takeIf { it.isNotBlank() } - return if (host != null && (scheme == "http" || scheme == "https")) { - activity.getString(CommonsR.string.browser_js_dialog_title, host) - } else { - activity.getString(CommonsR.string.browser_js_dialog_title_generic) - } - } -} diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserWebTools.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserWebTools.kt index 127bedaeda..7a31abd958 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserWebTools.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserWebTools.kt @@ -23,12 +23,12 @@ package com.vitorpamplona.amethyst.napplethost import android.app.Activity import android.content.ActivityNotFoundException import android.content.ClipData +import android.content.ClipDescription import android.content.ClipboardManager import android.content.ComponentName import android.content.Context import android.content.Intent import android.net.Uri -import android.net.http.SslCertificate import android.os.Build import android.os.SystemClock import android.webkit.CookieManager @@ -40,6 +40,7 @@ import androidx.core.net.toUri import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.browser.ui.pill.CertificateInfo import com.vitorpamplona.quartz.utils.Log import java.net.URISyntaxException import java.text.DateFormat @@ -266,37 +267,48 @@ object BrowserWebTools { // ---- page info ---- - /** The paragraphs of the page-info sheet for the page in [webView]. */ - fun pageInfo( - context: Context, - webView: WebView, - torOn: Boolean?, - ): String { - val url = webView.url.orEmpty() - val lines = mutableListOf<String>() - lines += - context.getString( - if (url.startsWith("https://", ignoreCase = true)) CommonsR.string.browser_page_info_https else CommonsR.string.browser_page_info_http, + /** The certificate of the page in [webView], for page info; null for a page without one. */ + fun certificateInfo(webView: WebView): CertificateInfo? = + webView.certificate?.let { cert -> + CertificateInfo( + issuedTo = cert.issuedTo?.cName?.takeIf { it.isNotBlank() } ?: cert.issuedTo?.oName.orEmpty(), + issuedBy = cert.issuedBy?.oName?.takeIf { it.isNotBlank() } ?: cert.issuedBy?.cName.orEmpty(), + validUntil = cert.validNotAfterDate?.let { DateFormat.getDateInstance(DateFormat.MEDIUM).format(it) }.orEmpty(), ) - if (torOn != null) { - lines += context.getString(if (torOn) CommonsR.string.browser_page_info_tor else CommonsR.string.browser_page_info_open_web) } - webView.certificate?.let { lines += certificateLine(context, it) } - return lines.joinToString("\n\n") - } - - private fun certificateLine( - context: Context, - cert: SslCertificate, - ): String { - val to = cert.issuedTo?.cName?.takeIf { it.isNotBlank() } ?: cert.issuedTo?.oName.orEmpty() - val by = cert.issuedBy?.oName?.takeIf { it.isNotBlank() } ?: cert.issuedBy?.cName.orEmpty() - val until = cert.validNotAfterDate?.let { DateFormat.getDateInstance(DateFormat.MEDIUM).format(it) }.orEmpty() - return context.getString(CommonsR.string.browser_page_info_certificate, to, by, until) - } // ---- copy / share / other browser ---- + /** Copies [text] with no confirmation of our own (Android 13+ shows one). */ + fun copyText( + context: Context, + label: String, + text: String, + ) { + context.getSystemService(ClipboardManager::class.java)?.setPrimaryClip(ClipData.newPlainText(label, text)) + } + + /** + * Whether "Paste and go" can be offered. Checks the clip's type only, never its contents, so Android + * doesn't toast a clipboard read every time the pill opens. + */ + fun clipboardHasText(context: Context): Boolean { + val description = context.getSystemService(ClipboardManager::class.java)?.primaryClipDescription ?: return false + return description.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN) || description.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML) + } + + /** The clipboard's text, trimmed; read only when the user asks to paste. */ + fun clipboardText(context: Context): String? = + context + .getSystemService(ClipboardManager::class.java) + ?.primaryClip + ?.takeIf { it.itemCount > 0 } + ?.getItemAt(0) + ?.coerceToText(context) + ?.toString() + ?.trim() + ?.takeIf { it.isNotEmpty() } + fun copyToClipboard( context: Context, text: String, diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt index 13b8c3069d..da634a5ecf 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt @@ -22,13 +22,13 @@ package com.vitorpamplona.amethyst.napplethost import android.Manifest import android.app.ActivityManager -import android.app.AlertDialog import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.content.pm.PackageManager import android.content.res.ColorStateList +import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.Color import android.graphics.drawable.Icon @@ -85,6 +85,10 @@ import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.Action import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission.Decision import com.vitorpamplona.amethyst.commons.browser.OmniboxInput +import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPillEvent +import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPillUi +import com.vitorpamplona.amethyst.commons.browser.ui.pill.ConsoleLine +import com.vitorpamplona.amethyst.commons.browser.ui.pill.PageDialogType import com.vitorpamplona.amethyst.commons.napplet.NappletWebContract import com.vitorpamplona.amethyst.commons.util.parseJsonObjectOrNull import com.vitorpamplona.amethyst.commons.util.stringOrNull @@ -108,7 +112,7 @@ import com.vitorpamplona.amethyst.commons.R as CommonsR * * PWA behaviours, beyond the page itself: its own task in Recents titled, iconed and coloured after the * site ([updateTaskDescription]); system bars tinted with the page's `theme-color`; the top pill - * ([NappletControlSheet], laid out by [BrowserChrome]); JS dialogs; new windows (`_blank` / `window.open`) + * ([BrowserChromeHost], laid out by [BrowserChrome]); JS dialogs; new windows (`_blank` / `window.open`) * as new browser windows with `opener` intact ([BrowserPopups]); downloads; HTML fullscreen video; * camera / microphone / location behind a per-site prompt; find in page; long-press link and image menus; * Web Share; and recovery from a renderer crash. @@ -130,10 +134,9 @@ class NappletBrowserActivity : ComponentActivity() { private var loadingView: View? = null private var crashView: View? = null private var resumed = false - private var controlSheet: NappletControlSheet? = null - private var consolePanel: NappletConsolePanel? = null - private var findBar: BrowserFindBar? = null - private var consoleShowing = false + + // The pill, find, console and page dialogs — the shared Compose chrome (see BrowserChromeHost). + private var chrome: BrowserChromeHost? = null // A thin determinate progress bar pinned to the top edge (browser-style), driven by the chrome // client's onProgressChanged; hidden at 100%. @@ -161,8 +164,10 @@ class NappletBrowserActivity : ComponentActivity() { private var customView: View? = null private var customViewCallback: WebChromeClient.CustomViewCallback? = null - // Page-originated alert/confirm/prompt/beforeunload, labelled with the page's origin. - private val jsDialogs by lazy { BrowserJsDialogs(this) } + // Page-originated alert/confirm/prompt/beforeunload: from the second dialog on a page, the user may block + // the rest until the next main-frame navigation (Chrome's rule), so a looping alert() can't trap them. + private var jsDialogsOnPage = 0 + private var jsDialogsBlocked = false // ---- HTML file input (`<input type="file">`) ---- // Registered as a field so it is in place before onCreate returns, which is what @@ -175,7 +180,6 @@ class NappletBrowserActivity : ComponentActivity() { // ---- site permissions (camera / microphone / location) ---- private val sitePermissionQueries = mutableMapOf<Long, (Map<BrowserSitePermission, Decision>) -> Unit>() private var sitePermissionSeq = 0L - private var permissionPrompt: AlertDialog? = null private var pendingRuntimeGrant: ((Map<String, Boolean>) -> Unit)? = null private val runtimePermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> @@ -230,7 +234,7 @@ class NappletBrowserActivity : ComponentActivity() { val wv = webView when { customView != null -> exitFullscreen() - findBar?.isShowing == true -> findBar?.hide() + chrome?.handleBack() == true -> Unit wv != null && wv.canGoBack() -> wv.goBack() else -> { isEnabled = false @@ -242,7 +246,7 @@ class NappletBrowserActivity : ComponentActivity() { } private fun syncBackState() { - backCallback.isEnabled = customView != null || findBar?.isShowing == true || webView?.canGoBack() == true + backCallback.isEnabled = customView != null || chrome?.wantsBack == true || webView?.canGoBack() == true } private val brokerConnection = @@ -304,18 +308,15 @@ class NappletBrowserActivity : ComponentActivity() { bindService(Intent().setClassName(this, NappletHostContract.BROKER_SERVICE_CLASS), brokerConnection, BIND_AUTO_CREATE) onBackPressedDispatcher.addCallback(this, backCallback) - val findBar = BrowserFindBar(this, { webView }) { syncBackState() }.also { this.findBar = it } val root = FrameLayout(this).apply { setBackgroundColor(resolveThemeColor(android.R.attr.colorBackground)) addView(contentFrame, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)) - addView(buildControlSheet(), FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.TOP)) - addView(buildConsolePanel(), FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM)) - addView(findBar, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM)) - // Added last so the thin loading bar paints above the content (and over the grabber's top edge). - addView(topProgressBar) } this.root = root + chrome = buildChrome().also { it.attach(root) } + // Added last so the thin loading bar paints above the content (and over the grabber's top edge). + root.addView(topProgressBar) setContentView(root) // Pad by the system bars + cutout AND the IME: on an edge-to-edge window (enforced for targetSdk // 35+ on Android 15+) windowSoftInputMode=adjustResize no longer shrinks the window, so without @@ -355,6 +356,7 @@ class NappletBrowserActivity : ComponentActivity() { BrowserWebTools.applyBrowserSettings(wv) wv.webViewClient = BrowserClient() wv.webChromeClient = BrowserChromeClient() + wv.setFindListener { active, total, _ -> chrome?.setFindResult(active, total) } wv.setDownloadListener { url, userAgent, contentDisposition, mimeType, _ -> BrowserDownloads.download(this, url, userAgent, contentDisposition, mimeType, BrowserWebTools.cookieManager(wv).getCookie(url), if (useTor) proxyPort else -1) } @@ -415,8 +417,8 @@ class NappletBrowserActivity : ComponentActivity() { pendingFileChooser.cancel() fileChooserLauncher.teardown() // A dialog still up would leak its window and leave the page's JS blocked on an unanswered result. - jsDialogs.dismiss() - permissionPrompt?.dismiss() + chrome?.dialog?.answer?.invoke(false, null, false) + chrome?.permissionPrompt?.answer?.invoke(false, false) customViewCallback?.onCustomViewHidden() destroyWebView() super.onDestroy() @@ -466,7 +468,19 @@ class NappletBrowserActivity : ComponentActivity() { runCatching { broker.send(msg) } } - private fun currentUrl(): String = webView?.url?.takeIf { it.isNotBlank() } ?: controlSheet?.state?.url?.takeIf { it.isNotBlank() } ?: startUrl + private fun currentUrl(): String = + webView?.url?.takeIf { it.isNotBlank() } ?: chrome + ?.ui + ?.chrome + ?.url + ?.takeIf { it.isNotBlank() } ?: startUrl + + /** Updates what the chrome shows. */ + private inline fun updateUi(block: BrowserPillUi.() -> BrowserPillUi) { + chrome?.let { it.ui = it.ui.block() } + } + + private inline fun updateChromeState(crossinline block: BrowserChrome.State.() -> BrowserChrome.State) = updateUi { copy(chrome = chrome.block()) } /** Captures favicon, title and console output, and hosts every page-initiated UI. */ private inner class BrowserChromeClient : WebChromeClient() { @@ -493,7 +507,7 @@ class NappletBrowserActivity : ComponentActivity() { title: String?, ) { pageTitle = title?.trim()?.takeIf { it.isNotEmpty() && it != view.url } - controlSheet?.updateTitle(title) + updateUi { copy(title = pageTitle ?: BrowserChrome.displayHost(chrome.url)) } updateTaskDescription() } @@ -512,34 +526,30 @@ class NappletBrowserActivity : ComponentActivity() { } override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { - val panel = consolePanel ?: return false - panel.appendLog( - consoleMessage.messageLevel(), - consoleMessage.message(), - consoleMessage.sourceId(), - consoleMessage.lineNumber(), + val host = chrome ?: return false + host.appendConsole( + ConsoleLine(BrowserChromeHost.levelOf(consoleMessage.messageLevel()), consoleMessage.message(), consoleMessage.sourceId(), consoleMessage.lineNumber()), ) - controlSheet?.updateConsoleCount(panel.entryCount) return true } // The framework's own JS dialogs only appear when the WebView's context IS an Activity // (`JsDialogHelper.canShowAlertDialog`), and this one is built from [nightThemedContext] — a // configuration context, not the Activity — so without these overrides every alert() was silently - // dismissed, confirm() always answered false and prompt() null. [jsDialogs] shows them itself. + // dismissed, confirm() always answered false and prompt() null. The chrome shows them itself. override fun onJsAlert( view: WebView, url: String?, message: String?, result: JsResult, - ): Boolean = jsDialogs.alert(url, message, result) + ): Boolean = showJsDialog(PageDialogType.ALERT, url, message, null, result) override fun onJsConfirm( view: WebView, url: String?, message: String?, result: JsResult, - ): Boolean = jsDialogs.confirm(url, message, result) + ): Boolean = showJsDialog(PageDialogType.CONFIRM, url, message, null, result) override fun onJsPrompt( view: WebView, @@ -547,14 +557,14 @@ class NappletBrowserActivity : ComponentActivity() { message: String?, defaultValue: String?, result: JsPromptResult, - ): Boolean = jsDialogs.prompt(url, message, defaultValue, result) + ): Boolean = showJsDialog(PageDialogType.PROMPT, url, message, defaultValue, result) override fun onJsBeforeUnload( view: WebView, url: String?, message: String?, result: JsResult, - ): Boolean = jsDialogs.beforeUnload(result) + ): Boolean = showJsDialog(PageDialogType.BEFORE_UNLOAD, url, message, null, result) /** * A `_blank` link or a user-initiated `window.open()`: open it as a new browser window, like a new @@ -583,7 +593,7 @@ class NappletBrowserActivity : ComponentActivity() { override fun onPermissionRequest(request: PermissionRequest) = handlePermissionRequest(request) override fun onPermissionRequestCanceled(request: PermissionRequest) { - permissionPrompt?.dismiss() + chrome?.permissionPrompt = null } override fun onGeolocationPermissionsShowPrompt( @@ -598,7 +608,7 @@ class NappletBrowserActivity : ComponentActivity() { } override fun onGeolocationPermissionsHidePrompt() { - permissionPrompt?.dismiss() + chrome?.permissionPrompt = null } override fun onShowCustomView( @@ -659,8 +669,9 @@ class NappletBrowserActivity : ComponentActivity() { applyThemeColor(null) } // Chrome scopes "block this page's dialogs" to the page: a new main-frame load lifts it. - jsDialogs.onMainFrameNavigation() - controlSheet?.setLoading(true) + jsDialogsOnPage = 0 + jsDialogsBlocked = false + updateChromeState { copy(isLoading = true) } showUrl(url) } @@ -707,7 +718,7 @@ class NappletBrowserActivity : ComponentActivity() { url: String, ) { syncNavigation(view) - controlSheet?.setLoading(false) + updateChromeState { copy(isLoading = false) } showUrl(url) // Record only a clean http(s) main-frame load — never a typed-but-failed address. if (!mainFrameLoadFailed && (url.startsWith("https://") || url.startsWith("http://"))) { @@ -741,7 +752,7 @@ class NappletBrowserActivity : ComponentActivity() { private fun syncNavigation(view: WebView) { syncBackState() - controlSheet?.setNavigation(view.canGoBack(), view.canGoForward()) + updateChromeState { copy(canGoBack = view.canGoBack(), canGoForward = view.canGoForward()) } } /** @@ -749,7 +760,16 @@ class NappletBrowserActivity : ComponentActivity() { * it is pinned — the registry lives in the main process, so the star can't know on its own. */ private fun showUrl(url: String) { - controlSheet?.updateUrl(url) + updateUi { + if (url == chrome.url) { + this + } else { + // Another site: its host names it until its title arrives; its pin state is unknown until the + // broker answers (the star toggle sends an explicit target, so a tap meanwhile can only add). + val newSite = BrowserChrome.displayHost(url) != BrowserChrome.displayHost(chrome.url) + copy(chrome = chrome.copy(url = url), title = if (newSite) BrowserChrome.displayHost(url) else title, isFavorite = false) + } + } if (url == lastFavoriteQueryUrl) return lastFavoriteQueryUrl = url val msg = @@ -835,7 +855,7 @@ class NappletBrowserActivity : ComponentActivity() { if (resolved.forceTor && proxyPort > 0 && !useTor) { useTor = true applyWebViewProxy(proxyPort) - controlSheet?.setTor(true) + updateChromeState { copy(torOn = true) } } webView?.loadUrl(resolved.url) } @@ -944,7 +964,8 @@ class NappletBrowserActivity : ComponentActivity() { } NappletIpc.MSG_WEB_FAVORITE_STATE -> { val url = data.getString(NappletIpc.KEY_FAVORITE_URL) ?: return true - controlSheet?.setFavorite(url, data.getBoolean(NappletIpc.KEY_FAVORITE_IS_FAVORITE, false)) + val favorite = data.getBoolean(NappletIpc.KEY_FAVORITE_IS_FAVORITE, false) + updateUi { if (url == chrome.url) copy(isFavorite = favorite) else this } } NappletIpc.MSG_SITE_PERMISSIONS -> { val callback = sitePermissionQueries.remove(data.getLong(NappletIpc.KEY_REQUEST_ID)) ?: return true @@ -1004,10 +1025,10 @@ class NappletBrowserActivity : ComponentActivity() { if (ask.isEmpty()) { ensureRuntimePermissions(allowed, done) } else { - showPermissionPrompt(origin, ask) { allow -> - // null = dismissed without an answer: deny this once, remember nothing. - if (allow != null) ask.forEach { rememberSitePermission(origin, it, if (allow) Decision.ALLOW else Decision.BLOCK) } - ensureRuntimePermissions(if (allow == true) allowed + ask else allowed, done) + showPermissionPrompt(origin, ask) { remembered, grantNow -> + // remembered: true/false = Allow while visiting / Don't allow; null = only this time or dismissed. + if (remembered != null) ask.forEach { rememberSitePermission(origin, it, if (remembered) Decision.ALLOW else Decision.BLOCK) } + ensureRuntimePermissions(if (grantNow) allowed + ask else allowed, done) } } } @@ -1049,49 +1070,33 @@ class NappletBrowserActivity : ComponentActivity() { } /** - * Chrome's permission bubble: "<host> wants to — Use your camera — Block / Allow". [answer] gets true - * (allow), false (block), or null when the prompt went away unanswered. + * The permission prompt ([com.vitorpamplona.amethyst.commons.browser.ui.pill.PermissionPromptCard]): + * [answer] gets true (allow, remembered), false (block, remembered), or null (allow only this time, or + * dismissed — nothing remembered; a dismissal also denies). */ private fun showPermissionPrompt( origin: String, permissions: List<BrowserSitePermission>, - answer: (Boolean?) -> Unit, + answer: (allow: Boolean?, grantNow: Boolean) -> Unit, ) { - if (isFinishing || isDestroyed || permissionPrompt != null) { - answer(null) + val host = chrome + if (host == null || isFinishing || isDestroyed || host.permissionPrompt != null) { + answer(null, false) return } - var answered = false - val lines = - permissions.joinToString("\n") { - "• " + - getString( - when (it) { - BrowserSitePermission.CAMERA -> CommonsR.string.browser_permission_camera - BrowserSitePermission.MICROPHONE -> CommonsR.string.browser_permission_microphone - BrowserSitePermission.LOCATION -> CommonsR.string.browser_permission_location - }, - ) + host.permissionPrompt = + BrowserChromeHost.PendingPermission( + host = BrowserChrome.displayHost(origin), + security = BrowserChrome.security(host.ui.chrome), + permissions = permissions.toSet(), + ) { allow, remember -> + when { + allow && remember -> answer(true, true) + allow -> answer(null, true) + remember -> answer(false, false) + else -> answer(null, false) + } } - permissionPrompt = - AlertDialog - .Builder(this) - .setTitle(getString(CommonsR.string.browser_permission_title, BrowserChrome.displayHost(origin))) - .setMessage(lines) - .setPositiveButton(CommonsR.string.browser_permission_allow) { _, _ -> - answered = true - answer(true) - }.setNegativeButton(CommonsR.string.browser_permission_block) { _, _ -> - answered = true - answer(false) - }.setOnDismissListener { - permissionPrompt = null - // Dismissed without choosing (back, the page cancelling): deny this time, remember nothing. - if (!answered) { - answered = true - answer(null) - } - }.show() } /** Requests Android's runtime permission for each allowed site permission that lacks it. */ @@ -1283,7 +1288,7 @@ class NappletBrowserActivity : ComponentActivity() { useTor = newUseTor applyWebViewProxy(if (useTor) proxyPort else -1) webView?.reload() - controlSheet?.setTor(useTor) + updateChromeState { copy(torOn = useTor) } // Key the persisted choice on the host actually displayed (which may differ from startUrl after // in-page navigation), so the preference sticks to the right site. val host = runCatching { currentUrl().toUri().host }.getOrNull()?.takeIf { it.isNotBlank() } ?: return @@ -1305,92 +1310,162 @@ class NappletBrowserActivity : ComponentActivity() { private fun barTitle(): String = title.ifBlank { runCatching { startUrl.toUri().host }.getOrNull() ?: getString(CommonsR.string.napplet_untitled) } /** - * The top pull-down pill: a small grabber at the top edge (out of the corner where a site shows its - * own avatar) that expands to the Chrome-PWA-style menu laid out by [BrowserChrome]. + * The window's chrome: the pull-down pill at the top (out of the corner where a site shows its own avatar), + * find and the console at the bottom, and the page's dialogs — the shared Compose components. */ - private fun buildControlSheet(): View = - NappletControlSheet( - context = this, - initialState = - BrowserChrome.State( - surface = BrowserChrome.Surface.WEB, - presentation = BrowserChrome.Presentation.FULL_SCREEN, - url = startUrl, - startUrl = startUrl, - torOn = if (proxyPort > 0) useTor else null, + private fun buildChrome(): BrowserChromeHost = + BrowserChromeHost( + activity = this, + dark = isDarkTheme(), + initial = + BrowserPillUi( + title = barTitle(), + chrome = + BrowserChrome.State( + surface = BrowserChrome.Surface.WEB, + presentation = BrowserChrome.Presentation.FULL_SCREEN, + url = startUrl, + startUrl = startUrl, + torOn = if (proxyPort > 0) useTor else null, + ), + isFavorite = intent.getBooleanExtra(EXTRA_IS_FAVORITE, false), ), - title = barTitle(), - listener = sheetListener, - isFavoriteInitially = intent.getBooleanExtra(EXTRA_IS_FAVORITE, false), - ).also { controlSheet = it } + listener = chromeListener, + ) - private val sheetListener = - object : NappletControlSheet.Listener { - override fun onAction(action: Action) { - val wv = webView - when (action) { - Action.BACK -> wv?.goBack() - Action.FORWARD -> wv?.goForward() - Action.RELOAD -> wv?.reload() - Action.STOP -> wv?.stopLoading() - Action.FAVORITE -> sendFavoriteToggle(currentUrl(), controlSheet?.wantsFavorite() ?: true) - Action.SHARE -> BrowserWebTools.share(this@NappletBrowserActivity, pageTitle, null, currentUrl()) - Action.BACK_TO_APP -> wv?.let { BrowserWebTools.backToScope(it, startUrl) } - Action.COPY_LINK -> BrowserWebTools.copyToClipboard(this@NappletBrowserActivity, currentUrl()) - Action.EDIT_ADDRESS -> Unit - Action.FIND_IN_PAGE -> { - setConsoleShowing(false) - findBar?.show() - syncBackState() + private fun isDarkTheme(): Boolean = + when (themeType) { + "DARK" -> true + "LIGHT" -> false + else -> (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES + } + + private val chromeListener = + object : BrowserChromeHost.Listener { + override fun onPillEvent(event: BrowserPillEvent) { + when (event) { + is BrowserPillEvent.Action -> onAction(event.action) + is BrowserPillEvent.Navigate -> loadAddress(event.input) + is BrowserPillEvent.TextZoom -> { + webView?.let { BrowserWebTools.setTextZoom(it, event.percent) } + updateUi { copy(textZoom = event.percent) } } - Action.TEXT_SIZE -> Unit - Action.DESKTOP_SITE -> - wv?.let { - val desktop = !BrowserWebTools.isDesktopMode(it) - BrowserWebTools.setDesktopMode(it, desktop) - controlSheet?.setDesktopSite(desktop) - } - Action.ADD_TO_HOME_SCREEN -> addToHomeScreen() - Action.OPEN_IN_BROWSER_APP -> BrowserWebTools.openInOtherBrowser(this@NappletBrowserActivity, currentUrl()) - Action.OPEN_FULL_SCREEN -> Unit - Action.TOR -> setNetworkMode(!useTor) - Action.ACCESS_INFO -> Unit - Action.SITE_SETTINGS -> openPermissions() - Action.CONSOLE -> setConsoleShowing(!consoleShowing) + BrowserPillEvent.CopyOrigin -> BrowserWebTools.copyToClipboard(this@NappletBrowserActivity, currentUrl()) + BrowserPillEvent.PageInfo -> showPageInfo() + BrowserPillEvent.Close -> finish() } } - override fun onNavigate(text: String) = loadAddress(text) - - override fun onTextZoom(percent: Int) { - webView?.let { BrowserWebTools.setTextZoom(it, percent) } + override fun onFind(query: String) { + val wv = webView ?: return + if (query.isEmpty()) wv.clearMatches() else wv.findAllAsync(query) } - override fun onOriginTap() = showPageInfo() + override fun onFindNext(forward: Boolean) { + webView?.findNext(forward) + } - override fun onClose() = finish() + override fun onFindClosed() { + webView?.clearMatches() + } + + override fun onPermissionChange( + permission: BrowserSitePermission, + decision: Decision, + ) { + BrowserChrome.originOf(currentUrl())?.let { rememberSitePermission(it, permission, decision) } + } + + override fun onClearSiteData() { + webView?.let { BrowserWebTools.clearSiteData(this@NappletBrowserActivity, it, currentUrl()) } + } + + override fun onPanelsChanged() = syncBackState() } - private fun setConsoleShowing(showing: Boolean) { - consoleShowing = showing - if (showing) findBar?.hide() - consolePanel?.setShowing(showing) - controlSheet?.setConsoleShowing(showing) + private fun onAction(action: Action) { + val wv = webView + when (action) { + Action.BACK -> wv?.goBack() + Action.FORWARD -> wv?.goForward() + Action.RELOAD -> wv?.reload() + Action.STOP -> wv?.stopLoading() + // The star flips the shown state; the toggle sends that target explicitly. + Action.FAVORITE -> sendFavoriteToggle(currentUrl(), chrome?.ui?.isFavorite != true) + Action.SHARE -> BrowserWebTools.share(this, pageTitle, null, currentUrl()) + Action.BACK_TO_APP -> wv?.let { BrowserWebTools.backToScope(it, startUrl) } + Action.COPY_LINK -> BrowserWebTools.copyToClipboard(this, currentUrl()) + Action.DESKTOP_SITE -> + wv?.let { + val desktop = !BrowserWebTools.isDesktopMode(it) + BrowserWebTools.setDesktopMode(it, desktop) + updateUi { copy(desktopSite = desktop) } + } + Action.ADD_TO_HOME_SCREEN -> addToHomeScreen() + Action.OPEN_IN_BROWSER_APP -> BrowserWebTools.openInOtherBrowser(this, currentUrl()) + Action.TOR -> setNetworkMode(!useTor) + Action.SITE_SETTINGS -> openPermissions() + else -> Unit + } } - /** Chrome's page-info sheet: connection, Tor, certificate, then site settings and clearing its data. */ + /** + * Shows a page's JS dialog in the chrome, titled with the page's host. Answers at once when dialogs are + * blocked for this page, another is already up, or the window is going away. + */ + private fun showJsDialog( + type: PageDialogType, + url: String?, + message: String?, + defaultValue: String?, + result: JsResult, + ): Boolean { + val host = chrome + if (jsDialogsBlocked) { + // A blocked page may no longer hold the user on it: leaving is allowed, everything else cancels. + if (type == PageDialogType.BEFORE_UNLOAD) result.confirm() else result.cancel() + return true + } + if (host == null || host.dialog != null || isFinishing || isDestroyed) { + result.cancel() + return true + } + jsDialogsOnPage++ + host.dialog = + BrowserChromeHost.PendingDialog( + type = type, + host = url?.let(BrowserChrome::originOf)?.let(BrowserChrome::displayHost), + security = BrowserChrome.security(host.ui.chrome), + message = message.orEmpty(), + defaultValue = defaultValue.orEmpty(), + offerBlock = jsDialogsOnPage > 1, + ) { confirmed, text, block -> + if (block) jsDialogsBlocked = true + when { + !confirmed -> result.cancel() + result is JsPromptResult -> result.confirm(text.orEmpty()) + else -> result.confirm() + } + } + return true + } + + /** + * Chrome's page info: connection, route and certificate, then this site's camera / microphone / location + * answers (asked of the broker first, so the sheet shows the truth) and its data. + */ private fun showPageInfo() { val wv = webView ?: return - val url = currentUrl() - AlertDialog - .Builder(this) - .setTitle(BrowserChrome.displayHost(url)) - .setMessage(BrowserWebTools.pageInfo(this, wv, if (proxyPort > 0) useTor else null)) - .setPositiveButton(android.R.string.ok, null) - .setNeutralButton(CommonsR.string.browser_page_info_permissions) { _, _ -> openPermissions() } - .setNegativeButton(CommonsR.string.browser_page_info_clear_data) { _, _ -> - webView?.let { BrowserWebTools.clearSiteData(this, it, url) } - }.show() + val origin = BrowserChrome.originOf(currentUrl()) + val certificate = BrowserWebTools.certificateInfo(wv) + if (origin == null) { + chrome?.showPageInfo(certificate) + return + } + querySitePermissions(origin) { decisions -> + updateUi { copy(sitePermissions = decisions.filterValues { it != Decision.ASK }) } + chrome?.showPageInfo(certificate) + } } /** Asks the main process to pin a launcher shortcut that reopens this page in Amethyst's browser. */ @@ -1446,12 +1521,6 @@ class NappletBrowserActivity : ComponentActivity() { queueToBroker(msg) } - private fun buildConsolePanel(): View = - NappletConsolePanel(this).also { - it.onClearCallback = { controlSheet?.updateConsoleCount(0) } - consolePanel = it - } - private fun buildLoadingView(): View = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL @@ -1521,10 +1590,11 @@ class NappletBrowserActivity : ComponentActivity() { private fun updateLoadProgress(progress: Int) { if (progress >= 100) { topProgressBar.visibility = View.GONE - controlSheet?.setLoading(false) + updateUi { copy(loadProgress = null, chrome = chrome.copy(isLoading = false)) } } else { topProgressBar.progress = progress topProgressBar.visibility = View.VISIBLE + updateUi { copy(loadProgress = progress / 100f) } } } @@ -1533,9 +1603,7 @@ class NappletBrowserActivity : ComponentActivity() { request: WebResourceRequest, message: String, ) { - val panel = consolePanel ?: return - panel.appendLog(ConsoleMessage.MessageLevel.ERROR, message, request.url?.toString().orEmpty(), 0) - controlSheet?.updateConsoleCount(panel.entryCount) + chrome?.appendConsole(ConsoleLine(ConsoleLine.Level.ERROR, message, request.url?.toString().orEmpty(), 0)) } private fun resolveThemeColor(attr: Int): Int { diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserContract.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserContract.kt index 01015fc67a..f814830628 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserContract.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserContract.kt @@ -145,10 +145,13 @@ object NappletBrowserContract { /** Clear the current site's cookies and storage in this account's profile, then reload. */ const val MSG_CLEAR_SITE_DATA = 24 - /** Ask for the page-info text (connection, Tor, certificate); answered with [MSG_PAGE_INFO]. */ + /** Ask for the page's certificate for page info; answered with [MSG_PAGE_INFO]. */ const val MSG_PAGE_INFO_REQUEST = 25 - /** Provider → client: [KEY_PAGE_INFO] for the page on screen. */ + /** + * Provider → client: the certificate of the page on screen — [KEY_CERT_ISSUED_TO], [KEY_CERT_ISSUED_BY] + * and [KEY_CERT_VALID_UNTIL], all absent for a page without one (plain HTTP). + */ const val MSG_PAGE_INFO = 26 /** @@ -188,7 +191,9 @@ object NappletBrowserContract { const val KEY_FIND_TOTAL = "findTotal" const val KEY_ENABLED = "enabled" const val KEY_TEXT_ZOOM = "textZoom" - const val KEY_PAGE_INFO = "pageInfo" + const val KEY_CERT_ISSUED_TO = "certIssuedTo" + const val KEY_CERT_ISSUED_BY = "certIssuedBy" + const val KEY_CERT_VALID_UNTIL = "certValidUntil" const val KEY_DIALOG_ID = "dialogId" const val KEY_DIALOG_TYPE = "dialogType" const val KEY_DIALOG_MESSAGE = "dialogMessage" diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserService.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserService.kt index aa5822da95..e58e8a861e 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserService.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserService.kt @@ -262,8 +262,13 @@ class NappletBrowserService : Service() { NappletBrowserContract.MSG_PAGE_INFO_REQUEST -> { val tab = tabFor(msg) ?: return true val wv = tab.webView ?: return true + val certificate = BrowserWebTools.certificateInfo(wv) sendToClient(tab, NappletBrowserContract.MSG_PAGE_INFO) { - putString(NappletBrowserContract.KEY_PAGE_INFO, BrowserWebTools.pageInfo(this@NappletBrowserService, wv, if (tab.proxyPort > 0) tab.useTor else null)) + certificate?.let { + putString(NappletBrowserContract.KEY_CERT_ISSUED_TO, it.issuedTo) + putString(NappletBrowserContract.KEY_CERT_ISSUED_BY, it.issuedBy) + putString(NappletBrowserContract.KEY_CERT_VALID_UNTIL, it.validUntil) + } } } NappletBrowserContract.MSG_JS_DIALOG_RESULT -> { diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletConsolePanel.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletConsolePanel.kt deleted file mode 100644 index 8c461caf09..0000000000 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletConsolePanel.kt +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.napplethost - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Color -import android.graphics.Typeface -import android.graphics.drawable.GradientDrawable -import android.util.TypedValue -import android.view.Gravity -import android.view.MotionEvent -import android.view.View -import android.webkit.ConsoleMessage -import android.widget.LinearLayout -import android.widget.ScrollView -import android.widget.TextView -import androidx.core.content.ContextCompat -import com.vitorpamplona.amethyst.commons.R as CommonsR - -/** - * The full-screen browser's **bottom pull-up sheet** for JavaScript console output. The whole sheet is - * hidden until the user flips the Console **toggle** in [NappletControlSheet] ([setShowing]); turned on, - * it reveals a small grabber at the bottom edge (symmetric to that sheet's top grabber) already pulled - * up. Pull it down/up (or tap) to collapse/expand the scrollable log of - * [console.log / warn / error / debug] messages captured from the page via - * `WebChromeClient.onConsoleMessage`. Capped at [MAX_ENTRIES] entries (oldest dropped on overflow). - * Built in plain Views like [NappletControlSheet] — no Compose/Material. - * - * Its grabber + panel are elevated above [NappletControlSheet]'s panel so that, when both are open at - * once (e.g. in landscape), this bottom sheet draws on top of the top pull-down sheet — mirroring the - * Compose layer, where `BottomConsoleSheet` is composed after `TopControlSheet`. - */ -@SuppressLint("UseSwitchCompatOrMaterialCode") -class NappletConsolePanel( - context: Context, -) : LinearLayout(context) { - private val onSurface = resolveThemeColor(android.R.attr.textColorPrimary) - private val dimmed = resolveThemeColor(android.R.attr.textColorSecondary) - private val surface = resolveThemeColor(android.R.attr.colorBackground) - - private var expanded = false - private var showing = false - private val panel: LinearLayout - private lateinit var logContainer: LinearLayout - private lateinit var scrollView: ScrollView - - init { - orientation = VERTICAL - gravity = Gravity.CENTER_HORIZONTAL - // Hidden until the Console toggle turns it on; matches the Compose `BottomConsoleSheet`, which is - // only composed while the toggle is on. - visibility = View.GONE - - panel = buildPanel().also { addView(it) } - addView(buildGrabber()) - } - - private fun buildPanel(): LinearLayout = - LinearLayout(context).apply { - orientation = VERTICAL - visibility = View.GONE - // Above NappletControlSheet's panel (6dp) so an open console draws over an open top sheet. - elevation = dp(8).toFloat() - background = - GradientDrawable().apply { - cornerRadii = floatArrayOf(dp(16).toFloat(), dp(16).toFloat(), dp(16).toFloat(), dp(16).toFloat(), 0f, 0f, 0f, 0f) - setColor(surface) - } - setPadding(dp(8), dp(10), dp(8), dp(6)) - layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dp(220)) - - val headerRow = - LinearLayout(context).apply { - orientation = HORIZONTAL - gravity = Gravity.CENTER_VERTICAL - setPadding(dp(8), 0, dp(4), dp(4)) - addView( - TextView(context).apply { - text = context.getString(CommonsR.string.browser_console_title_short) - setTextColor(dimmed) - textSize = 12f - layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f) - }, - ) - addView( - TextView(context).apply { - text = context.getString(CommonsR.string.browser_console_clear) - setTextColor(dimmed) - textSize = 12f - setPadding(dp(12), dp(6), dp(12), dp(6)) - isClickable = true - setOnClickListener { clearLogs() } - }, - ) - } - - val container = - LinearLayout(context).apply { - orientation = VERTICAL - } - logContainer = container - - val sv = - ScrollView(context).apply { - addView(container, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)) - layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f) - } - scrollView = sv - - addView(headerRow) - addView(sv) - } - - /** Number of log entries currently stored (used to update the control sheet count label). */ - var entryCount: Int = 0 - private set - - /** - * Shows or hides the entire sheet (grabber + log), driven by the control sheet's Console **toggle**: - * off hides everything, on reveals the sheet already pulled up — mirroring the Compose - * `BottomConsoleSheet`, which is only composed while the toggle is on and opens expanded. - */ - fun setShowing(show: Boolean) { - if (show == showing) return - showing = show - if (show) { - visibility = View.VISIBLE - expand() - } else { - collapse() - visibility = View.GONE - } - } - - fun appendLog( - level: ConsoleMessage.MessageLevel, - message: String, - source: String, - lineNumber: Int, - ) { - if (entryCount >= MAX_ENTRIES && logContainer.childCount > 0) { - logContainer.removeViewAt(0) - } else { - entryCount++ - } - - val levelChar = - when (level) { - ConsoleMessage.MessageLevel.ERROR -> "E" - ConsoleMessage.MessageLevel.WARNING -> "W" - ConsoleMessage.MessageLevel.TIP -> "T" - ConsoleMessage.MessageLevel.DEBUG -> "D" - else -> "I" - } - val levelColor = - when (level) { - ConsoleMessage.MessageLevel.ERROR -> Color.RED - ConsoleMessage.MessageLevel.WARNING -> Color.rgb(255, 152, 0) - ConsoleMessage.MessageLevel.TIP -> Color.CYAN - ConsoleMessage.MessageLevel.DEBUG -> dimmed - else -> onSurface - } - - val srcShort = - source - .substringAfterLast("/") - .substringAfterLast("\\") - .let { if (it.isBlank()) source.takeLast(20) else it } - val annotation = if (srcShort.isNotBlank()) " ($srcShort:$lineNumber)" else "" - - val entry = - TextView(context).apply { - text = "$levelChar $message$annotation" - setTextColor(levelColor) - textSize = 11f - typeface = Typeface.MONOSPACE - setPadding(dp(4), dp(2), dp(4), dp(2)) - } - logContainer.addView(entry) - scrollView.post { scrollView.fullScroll(View.FOCUS_DOWN) } - } - - private fun clearLogs() { - logContainer.removeAllViews() - entryCount = 0 - // Return a callback so the activity can update the control sheet count after clearing. - onClearCallback?.invoke() - } - - var onClearCallback: (() -> Unit)? = null - - /** The grabber: a small rounded bar centered at the bottom edge. Tap toggles, vertical drag opens/closes. */ - @SuppressLint("ClickableViewAccessibility") - private fun buildGrabber(): View { - val bar = - View(context).apply { - background = - GradientDrawable().apply { - cornerRadius = dp(3).toFloat() - setColor(dimmed and 0x99FFFFFF.toInt()) - } - layoutParams = LayoutParams(dp(36), dp(5)) - } - return LinearLayout(context).apply { - orientation = VERTICAL - gravity = Gravity.CENTER_HORIZONTAL - layoutParams = - LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply { - gravity = Gravity.CENTER_HORIZONTAL - } - setPadding(dp(16), dp(7), dp(16), dp(7)) - // Above NappletControlSheet's panel (6dp) so the grabber stays on top of an open top sheet. - elevation = dp(8).toFloat() - background = - GradientDrawable().apply { - cornerRadii = floatArrayOf(dp(12).toFloat(), dp(12).toFloat(), dp(12).toFloat(), dp(12).toFloat(), 0f, 0f, 0f, 0f) - setColor(withAlpha(surface, 0.6f)) - } - isClickable = true - contentDescription = context.getString(CommonsR.string.browser_console_title_short) - addView(bar) - - var downY = 0f - var dragged = false - setOnTouchListener { _, ev -> - when (ev.actionMasked) { - MotionEvent.ACTION_DOWN -> { - downY = ev.rawY - dragged = false - true - } - MotionEvent.ACTION_MOVE -> { - val dy = ev.rawY - downY - if (dy < -dp(8)) { - expand() - dragged = true - } else if (dy > dp(8)) { - collapse() - dragged = true - } - true - } - MotionEvent.ACTION_UP -> { - if (!dragged) { - if (expanded) collapse() else expand() - } - true - } - else -> false - } - } - } - } - - private fun expand() { - if (expanded) return - expanded = true - panel.visibility = View.VISIBLE - } - - private fun collapse() { - if (!expanded) return - expanded = false - panel.visibility = View.GONE - } - - private fun withAlpha( - color: Int, - alpha: Float, - ): Int = (color and 0x00FFFFFF) or ((alpha * 255).toInt() shl 24) - - private fun resolveThemeColor(attr: Int): Int { - val tv = TypedValue() - context.theme.resolveAttribute(attr, tv, true) - return if (tv.resourceId != 0) ContextCompat.getColor(context, tv.resourceId) else tv.data.takeIf { it != 0 } ?: Color.GRAY - } - - private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt() - - private companion object { - private const val MAX_ENTRIES = 200 - } -} diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletControlSheet.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletControlSheet.kt deleted file mode 100644 index d1546410bf..0000000000 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletControlSheet.kt +++ /dev/null @@ -1,647 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.napplethost - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Color -import android.graphics.Typeface -import android.graphics.drawable.GradientDrawable -import android.text.InputType -import android.text.TextUtils -import android.util.TypedValue -import android.view.Gravity -import android.view.MotionEvent -import android.view.View -import android.view.inputmethod.EditorInfo -import android.view.inputmethod.InputMethodManager -import android.widget.EditText -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.ScrollView -import android.widget.Switch -import android.widget.TextView -import androidx.core.content.ContextCompat -import com.vitorpamplona.amethyst.commons.browser.BrowserChrome -import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.Action -import com.vitorpamplona.amethyst.commons.browser.BrowserChrome.SectionKind -import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol -import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols -import com.vitorpamplona.amethyst.commons.R as CommonsR - -/** - * The full-screen surfaces' **top pull-down pill** — the plain-View twin of the embedded tabs' Compose - * `TopControlSheet`. Collapsed it's a small grabber centered at the top edge, out of the corner where a site - * puts its own avatar/menu. Pulled down (or tapped) it shows, like a Chrome PWA's app menu: - * - * - a header with the page title, its origin + connection badge (tap: page info; long-press: copy link), - * and a close button; - * - the icon row (back · forward · reload/stop · star · share); - * - the menu rows, then Privacy and Developer groups. - * - * *Which* actions appear, and in what order, comes from [BrowserChrome] — the same source the Compose sheet - * uses — and each action's icon/label from [BrowserChromeLabels]. This class only draws them. The page - * can't draw over it. Built in code (no XML, no Compose/Material) because `:nappletHost` stays light. - */ -@SuppressLint("UseSwitchCompatOrMaterialCode", "ViewConstructor") -class NappletControlSheet( - context: Context, - initialState: BrowserChrome.State, - title: String, - private val listener: Listener, - isFavoriteInitially: Boolean = false, - /** Shows the header's close button (finishing the window). */ - private val showClose: Boolean = true, -) : LinearLayout(context) { - /** What the sheet asks its host to do. Every row and icon ends up in [onAction]. */ - interface Listener { - fun onAction(action: Action) - - /** The user typed an address into "Edit address" and pressed Go. */ - fun onNavigate(text: String) {} - - /** The text-size stepper moved to [percent]. */ - fun onTextZoom(percent: Int) {} - - /** The origin chip was tapped: show page info (or the access summary for sandboxed apps). */ - fun onOriginTap() {} - - fun onClose() {} - } - - private val onSurface = resolveThemeColor(android.R.attr.textColorPrimary) - private val dimmed = resolveThemeColor(android.R.attr.textColorSecondary) - private val surface = resolveThemeColor(android.R.attr.colorBackground) - private val accent = resolveThemeColor(android.R.attr.colorPrimary) - private val glyphs: Typeface = BrowserGlyphs.typeface(context) - - var state: BrowserChrome.State = initialState - private set - private var title: String = title - private var isFavorite = isFavoriteInitially - private var desktopSite = false - private var textZoom = BrowserChrome.DEFAULT_TEXT_ZOOM - private var consoleShowing = false - private var consoleCount = 0 - private var editingAddress = false - - private var expanded = false - private val panel: LinearLayout - private val grabber: View - - init { - orientation = VERTICAL - gravity = Gravity.CENTER_HORIZONTAL - panel = - LinearLayout(context).apply { - orientation = VERTICAL - visibility = View.GONE - elevation = dp(6).toFloat() - background = - GradientDrawable().apply { - cornerRadii = floatArrayOf(0f, 0f, 0f, 0f, dp(16).toFloat(), dp(16).toFloat(), dp(16).toFloat(), dp(16).toFloat()) - setColor(surface) - } - setPadding(dp(8), dp(6), dp(8), dp(10)) - } - addView(panel) - grabber = buildGrabber() - addView(grabber) - } - - // ---- state updates from the host ---- - - /** - * The page navigated. Moving to another site swaps the title for that site's host until its page title - * arrives ([updateTitle]), and marks the pin state unknown until the host answers ([setFavorite]). - */ - fun updateUrl(url: String) { - val previous = state.url - if (url == previous) return - state = state.copy(url = url) - if (BrowserChrome.displayHost(url) != BrowserChrome.displayHost(previous)) title = BrowserChrome.displayHost(url) - // Unknown until the host answers; "Add" meanwhile. The toggle sends an explicit target state, so a - // tap in that window can only add (idempotent), never silently remove an existing pin. - isFavorite = false - refresh() - } - - /** Shows the page's `<title>`, falling back to the host (WebView reports the URL for untitled pages). */ - fun updateTitle(pageTitle: String?) { - val real = pageTitle?.trim()?.takeIf { it.isNotEmpty() && it != state.url } - title = real ?: BrowserChrome.displayHost(state.url) - refresh() - } - - /** Applies the registry's answer for [url]; ignored once the user has moved on to another page. */ - fun setFavorite( - url: String, - favorite: Boolean, - ) { - if (url != state.url) return - isFavorite = favorite - refresh() - } - - fun setNavigation( - canGoBack: Boolean, - canGoForward: Boolean, - ) = update(state.copy(canGoBack = canGoBack, canGoForward = canGoForward)) - - fun setLoading(loading: Boolean) = update(state.copy(isLoading = loading)) - - fun setTor(on: Boolean) = update(state.copy(torOn = on)) - - fun setDesktopSite(on: Boolean) { - desktopSite = on - refresh() - } - - fun setTextZoom(percent: Int) { - textZoom = percent - refresh() - } - - fun setConsoleShowing(showing: Boolean) { - consoleShowing = showing - refresh() - } - - fun updateConsoleCount(count: Int) { - consoleCount = count - refresh() - } - - private fun update(next: BrowserChrome.State) { - if (next == state) return - state = next - refresh() - } - - /** Rebuilds the open panel. Collapsed, nothing is drawn, so it waits for the next [expand]. */ - private fun refresh() { - grabber.contentDescription = title - if (expanded) render() - } - - // ---- drawing ---- - - private fun render() { - panel.removeAllViews() - panel.addView(header()) - panel.addView(iconRow()) - panel.addView(divider()) - val sections = - LinearLayout(context).apply { - orientation = VERTICAL - BrowserChrome.sections(state).forEachIndexed { index, section -> - if (index > 0) addView(divider()) - sectionTitle(section.kind)?.let { addView(sectionLabel(it)) } - section.actions.forEach { addView(row(it)) } - } - } - panel.addView( - MaxHeightScrollView(context, (resources.displayMetrics.heightPixels * 0.6f).toInt()).apply { - isVerticalScrollBarEnabled = false - addView(sections) - }, - ) - } - - private fun sectionTitle(kind: SectionKind): String? = - when (kind) { - SectionKind.PAGE -> null - SectionKind.PRIVACY -> context.getString(CommonsR.string.browser_section_privacy) - SectionKind.DEVELOPER -> context.getString(CommonsR.string.browser_section_developer) - } - - private fun header(): View = - LinearLayout(context).apply { - orientation = HORIZONTAL - gravity = Gravity.CENTER_VERTICAL - setPadding(dp(8), dp(6), dp(4), dp(6)) - val security = BrowserChrome.security(state) - addView(iconView(BrowserChromeLabels.securitySymbol(security), BrowserChromeLabels.securityDrawable(security), dimmed, 20)) - if (editingAddress) { - addView(addressField()) - } else { - addView( - LinearLayout(context).apply { - orientation = VERTICAL - setPadding(dp(12), 0, dp(8), 0) - layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f) - isClickable = true - setOnClickListener { - collapse() - listener.onOriginTap() - } - if (!state.isSandbox) { - setOnLongClickListener { - listener.onAction(Action.COPY_LINK) - true - } - } - addView( - TextView(context).apply { - text = title - setTextColor(onSurface) - textSize = 16f - typeface = Typeface.DEFAULT_BOLD - maxLines = 1 - ellipsize = TextUtils.TruncateAt.END - }, - ) - addView( - TextView(context).apply { - text = - if (state.isSandbox) { - context.getString(BrowserChromeLabels.securityLabel(security)) - } else { - BrowserChrome.displayHost(state.url) + " · " + context.getString(BrowserChromeLabels.securityLabel(security)) - } - setTextColor(dimmed) - textSize = 13f - maxLines = 1 - ellipsize = TextUtils.TruncateAt.MIDDLE - }, - ) - }, - ) - } - if (showClose) { - addView( - glyphButton(BrowserChromeLabels.symbolFor(Action.STOP)!!, context.getString(CommonsR.string.browser_action_close), onSurface) { - collapse() - listener.onClose() - }, - ) - } - } - - /** The rarely used editable address, swapped in for the origin chip by the "Edit address" row. */ - private fun addressField(): View { - val field = - EditText(context).apply { - setText(state.url) - setTextColor(onSurface) - setHintTextColor(dimmed) - hint = context.getString(CommonsR.string.browser_address_hint) - contentDescription = context.getString(CommonsR.string.browser_address_hint) - textSize = 15f - isSingleLine = true - setSelectAllOnFocus(true) - background = null - inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI - imeOptions = EditorInfo.IME_ACTION_GO - layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f).apply { marginStart = dp(8) } - setOnEditorActionListener { v, actionId, _ -> - if (actionId != EditorInfo.IME_ACTION_GO) return@setOnEditorActionListener false - val text = - v.text - ?.toString() - ?.trim() - .orEmpty() - if (text.isNotEmpty()) { - hideKeyboard(v) - editingAddress = false - collapse() - listener.onNavigate(text) - } - true - } - } - field.post { - field.requestFocus() - context.getSystemService(InputMethodManager::class.java)?.showSoftInput(field, 0) - } - return field - } - - private fun iconRow(): View = - LinearLayout(context).apply { - orientation = HORIZONTAL - gravity = Gravity.CENTER_VERTICAL - setPadding(0, dp(2), 0, dp(4)) - BrowserChrome.iconRow(state).forEach { action -> - val enabled = BrowserChrome.isEnabled(state, action) - val tint = if (action == Action.FAVORITE && isFavorite) accent else onSurface - val button = - glyphButton( - BrowserChromeLabels.symbolFor(action)!!, - context.getString(BrowserChromeLabels.labelFor(action, isFavorite = isFavorite)), - tint, - ) { - collapse() - onRowAction(action) - } - if (action == Action.FAVORITE && isFavorite) button.typeface = BrowserGlyphs.filledTypeface(context) - button.isEnabled = enabled - button.alpha = if (enabled) 1f else 0.35f - button.layoutParams = LayoutParams(0, dp(48), 1f) - addView(button) - } - } - - private fun row(action: Action): View { - val label = - when (action) { - Action.BACK_TO_APP -> context.getString(CommonsR.string.browser_action_back_to_app, BrowserChrome.displayHost(state.startUrl)) - else -> context.getString(BrowserChromeLabels.labelFor(action, isFavorite = isFavorite, torOn = state.torOn == true)) - } - val consoleLabel = if (action == Action.CONSOLE && consoleCount > 0) context.getString(CommonsR.string.browser_console_title, consoleCount) else label - return LinearLayout(context).apply { - orientation = HORIZONTAL - gravity = Gravity.CENTER_VERTICAL - setPadding(dp(8), dp(10), dp(8), dp(10)) - addView(iconView(BrowserChromeLabels.symbolFor(action), BrowserChromeLabels.drawableFor(action), dimmed, 22)) - addView( - TextView(context).apply { - text = consoleLabel - setTextColor(onSurface) - textSize = 15f - setPadding(dp(14), 0, 0, 0) - layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f) - }, - ) - if (action == Action.TEXT_SIZE) { - addView(textSizeStepper()) - } else { - isClickable = true - setOnClickListener { - if (!BrowserChromeLabels.keepsSheetOpen(action)) collapse() - onRowAction(action) - } - if (BrowserChromeLabels.isToggle(action)) { - // Display-only: the whole row is the touch target, as in the Compose twin. - addView( - Switch(context).apply { - isChecked = - when (action) { - Action.TOR -> state.torOn == true - Action.DESKTOP_SITE -> desktopSite - else -> consoleShowing - } - isClickable = false - isFocusable = false - }, - ) - } - } - } - } - - private fun textSizeStepper(): View = - LinearLayout(context).apply { - orientation = HORIZONTAL - gravity = Gravity.CENTER_VERTICAL - addView( - glyphButton(MaterialSymbols.Remove, context.getString(CommonsR.string.browser_action_text_smaller), onSurface) { - textZoom = BrowserChrome.stepTextZoom(textZoom, larger = false) - listener.onTextZoom(textZoom) - refresh() - }, - ) - addView( - TextView(context).apply { - text = context.getString(CommonsR.string.browser_action_text_size_value, textZoom) - setTextColor(onSurface) - textSize = 14f - gravity = Gravity.CENTER - minWidth = dp(48) - }, - ) - addView( - glyphButton(MaterialSymbols.Add, context.getString(CommonsR.string.browser_action_text_larger), onSurface) { - textZoom = BrowserChrome.stepTextZoom(textZoom, larger = true) - listener.onTextZoom(textZoom) - refresh() - }, - ) - } - - private fun onRowAction(action: Action) { - when (action) { - Action.EDIT_ADDRESS -> { - editingAddress = true - refresh() - } - Action.FAVORITE -> { - isFavorite = !isFavorite - listener.onAction(action) - } - Action.DESKTOP_SITE -> { - desktopSite = !desktopSite - listener.onAction(action) - } - Action.CONSOLE -> { - consoleShowing = !consoleShowing - listener.onAction(action) - } - else -> listener.onAction(action) - } - } - - /** The favorite state the user is asking for (read by the host right after a FAVORITE action). */ - fun wantsFavorite(): Boolean = isFavorite - - private fun sectionLabel(text: String): View = - TextView(context).apply { - this.text = text - setTextColor(dimmed) - textSize = 12f - isAllCaps = true - letterSpacing = 0.06f - setPadding(dp(8), dp(10), dp(8), dp(2)) - } - - private fun iconView( - symbol: MaterialSymbol?, - drawable: Int?, - tint: Int, - sizeDp: Int, - ): View = - if (drawable != null) { - ImageView(context).apply { - setImageResource(drawable) - setColorFilter(tint) - layoutParams = LayoutParams(dp(sizeDp), dp(sizeDp)) - } - } else { - glyphText(symbol?.glyph.orEmpty(), tint, sizeDp).apply { layoutParams = LayoutParams(dp(sizeDp + 4), LayoutParams.WRAP_CONTENT) } - } - - private fun glyphText( - glyph: String, - tint: Int, - sizeDp: Int, - ): TextView = - TextView(context).apply { - text = glyph - typeface = glyphs - setTextColor(tint) - setTextSize(TypedValue.COMPLEX_UNIT_DIP, sizeDp.toFloat()) - gravity = Gravity.CENTER - includeFontPadding = false - } - - private fun glyphButton( - symbol: MaterialSymbol, - description: String, - tint: Int, - onClick: () -> Unit, - ): TextView = - glyphText(symbol.glyph, tint, 22).apply { - contentDescription = description - tooltipText = description - minWidth = dp(44) - minHeight = dp(44) - isClickable = true - isFocusable = true - background = selectableBackground() - // Mirrored glyphs (back/forward) flip for right-to-left layouts, as Compose's autoMirror does. - if (symbol.autoMirror && resources.configuration.layoutDirection == LAYOUT_DIRECTION_RTL) scaleX = -1f - setOnClickListener { onClick() } - } - - private fun selectableBackground() = - TypedValue().let { tv -> - context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, tv, true) - ContextCompat.getDrawable(context, tv.resourceId) - } - - private fun divider(): View = - View(context).apply { - setBackgroundColor(dimmed and 0x33FFFFFF) - layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dp(1)).apply { setMargins(0, dp(2), 0, dp(2)) } - } - - private fun hideKeyboard(view: View) { - context.getSystemService(InputMethodManager::class.java)?.hideSoftInputFromWindow(view.windowToken, 0) - } - - /** The grabber: a small rounded bar centered at the top edge; tap toggles, vertical drag opens/closes. */ - @SuppressLint("ClickableViewAccessibility") - private fun buildGrabber(): View { - val bar = - View(context).apply { - background = - GradientDrawable().apply { - cornerRadius = dp(3).toFloat() - setColor(dimmed and 0x99FFFFFF.toInt()) - } - layoutParams = LayoutParams(dp(36), dp(5)) - } - return LinearLayout(context).apply { - orientation = VERTICAL - gravity = Gravity.CENTER_HORIZONTAL - layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply { gravity = Gravity.CENTER_HORIZONTAL } - setPadding(dp(16), dp(7), dp(16), dp(7)) - background = - GradientDrawable().apply { - cornerRadii = floatArrayOf(0f, 0f, 0f, 0f, dp(12).toFloat(), dp(12).toFloat(), dp(12).toFloat(), dp(12).toFloat()) - setColor(withAlpha(surface, 0.6f)) - } - isClickable = true - contentDescription = title - addView(bar) - - var downY = 0f - var dragged = false - setOnTouchListener { _, ev -> - when (ev.actionMasked) { - MotionEvent.ACTION_DOWN -> { - downY = ev.rawY - dragged = false - true - } - MotionEvent.ACTION_MOVE -> { - val dy = ev.rawY - downY - if (dy > dp(8)) { - expand() - dragged = true - } else if (dy < -dp(8)) { - collapse() - dragged = true - } - true - } - MotionEvent.ACTION_UP -> { - if (!dragged) { - if (expanded) { - collapse() - } else { - expand() - } - } - true - } - else -> false - } - } - } - } - - val isExpanded: Boolean get() = expanded - - fun expand() { - if (expanded) return - expanded = true - render() - panel.visibility = View.VISIBLE - } - - fun collapse() { - if (!expanded) return - expanded = false - if (editingAddress) { - editingAddress = false - hideKeyboard(panel) - } - panel.visibility = View.GONE - panel.removeAllViews() - } - - private fun withAlpha( - color: Int, - alpha: Float, - ): Int = (color and 0x00FFFFFF) or ((alpha * 255).toInt() shl 24) - - private fun resolveThemeColor(attr: Int): Int { - val tv = TypedValue() - context.theme.resolveAttribute(attr, tv, true) - return if (tv.resourceId != 0) ContextCompat.getColor(context, tv.resourceId) else tv.data.takeIf { it != 0 } ?: Color.GRAY - } - - private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt() - - /** A [ScrollView] that stops growing at [maxHeightPx], so a long menu scrolls instead of covering the page. */ - private class MaxHeightScrollView( - context: Context, - private val maxHeightPx: Int, - ) : ScrollView(context) { - override fun onMeasure( - widthMeasureSpec: Int, - heightMeasureSpec: Int, - ) { - super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(maxHeightPx, MeasureSpec.AT_MOST)) - } - } -} diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt index ca4d8ddc56..b16ab84463 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt @@ -26,6 +26,7 @@ import android.content.ComponentName import android.content.Intent import android.content.ServiceConnection import android.content.res.ColorStateList +import android.content.res.Configuration import android.net.Uri import android.os.Bundle import android.os.Handler @@ -64,6 +65,10 @@ import androidx.webkit.WebMessageCompat import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission +import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPillEvent +import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPillUi +import com.vitorpamplona.amethyst.commons.browser.ui.pill.ConsoleLine import com.vitorpamplona.amethyst.commons.napplet.NappletWebContract import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletProtocolJson import com.vitorpamplona.amethyst.commons.util.booleanOrNull @@ -194,11 +199,8 @@ class NappletHostActivity : ComponentActivity() { // WebChromeClient's onProgressChanged; hidden at 100%. private val topProgressBar by lazy { buildTopProgressBar() } - // Bottom pull-up developer console: the page's console.log/warn/error plus any resource load errors. - private var consolePanel: NappletConsolePanel? = null - private var controlSheet: NappletControlSheet? = null - private var findBar: BrowserFindBar? = null - private var consoleShowing = false + // The trusted pull-down pill, find and the developer console — the shared Compose chrome. + private var chrome: BrowserChromeHost? = null // Set when the renderer died and the WebView was destroyed, so teardown doesn't touch it again. private var webViewGone = false @@ -211,8 +213,8 @@ class NappletHostActivity : ComponentActivity() { private val backCallback = object : OnBackPressedCallback(false) { override fun handleOnBackPressed() { - if (findBar?.isShowing == true) { - findBar?.hide() + if (chrome?.handleBack() == true) { + Unit } else if (this@NappletHostActivity::webView.isInitialized && !webViewGone && webView.canGoBack()) { webView.goBack() } else { @@ -225,7 +227,7 @@ class NappletHostActivity : ComponentActivity() { /** Keep the in-WebView back gesture enabled exactly while the applet has history to pop. */ private fun syncBackState() { val canGoBack = this::webView.isInitialized && !webViewGone && webView.canGoBack() - backCallback.isEnabled = canGoBack || findBar?.isShowing == true + backCallback.isEnabled = canGoBack || chrome?.wantsBack == true } // True between onResume and onPause. Sent to the broker (foreground hold) on connect too, in case @@ -294,6 +296,7 @@ class NappletHostActivity : ComponentActivity() { // profile has otherwise been used), so the storage partition must be chosen before anything else. NappletWebViewProfile.apply(this, webView, webViewProfile) hardenWebView(webView) + webView.setFindListener { active, total, _ -> chrome?.setFindResult(active, total) } // Theme the WebView's pre-paint background to the app's so it doesn't flash white when the shell // mounts. This activity has a themed context, so it resolves the color locally (no IPC needed). webView.setBackgroundColor(resolveThemeColor(android.R.attr.colorBackground)) @@ -321,33 +324,11 @@ class NappletHostActivity : ComponentActivity() { val root = FrameLayout(this).apply { addView(contentFrame, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)) - addView( - buildControlSheet(), - FrameLayout - .LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.WRAP_CONTENT, - Gravity.TOP, - ), - ) - addView( - buildConsolePanel(), - FrameLayout - .LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.WRAP_CONTENT, - Gravity.BOTTOM, - ), - ) - addView( - BrowserFindBar(this@NappletHostActivity, { if (this@NappletHostActivity::webView.isInitialized && !webViewGone) webView else null }) { syncBackState() } - .also { findBar = it }, - FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM), - ) - // Added last so the thin loading bar paints above the content (and over the grabber's top - // edge); it's GONE except while loading, so it never obscures the trusted chrome. - addView(topProgressBar) } + chrome = buildChrome().also { it.attach(root) } + // Added last so the thin loading bar paints above the content (and over the grabber's top edge); it's + // GONE except while loading, so it never obscures the trusted chrome. + root.addView(topProgressBar) setContentView(root) // Activities are edge-to-edge by default on recent Android; pad by the system bar, display-cutout // and IME insets so neither the chrome nor the applet draws under the system bars or the soft @@ -669,7 +650,7 @@ class NappletHostActivity : ComponentActivity() { ): Boolean { Log.w(TAG) { "Renderer gone (crashed=${detail.didCrash()}); offering a restart" } webViewGone = true - findBar?.hide() + chrome?.closeFind() (view.parent as? ViewGroup)?.removeView(view) view.destroy() loadingView?.let { contentFrame.removeView(it) } @@ -721,9 +702,10 @@ class NappletHostActivity : ComponentActivity() { } override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { - val panel = consolePanel ?: return false - panel.appendLog(consoleMessage.messageLevel(), consoleMessage.message(), consoleMessage.sourceId(), consoleMessage.lineNumber()) - controlSheet?.updateConsoleCount(panel.entryCount) + val host = chrome ?: return false + host.appendConsole( + ConsoleLine(BrowserChromeHost.levelOf(consoleMessage.messageLevel()), consoleMessage.message(), consoleMessage.sourceId(), consoleMessage.lineNumber()), + ) return true } } @@ -751,9 +733,11 @@ class NappletHostActivity : ComponentActivity() { private fun updateLoadProgress(progress: Int) { if (progress >= 100) { topProgressBar.visibility = View.GONE + chrome?.let { it.ui = it.ui.copy(loadProgress = null, chrome = it.ui.chrome.copy(isLoading = false)) } } else { topProgressBar.progress = progress topProgressBar.visibility = View.VISIBLE + chrome?.let { it.ui = it.ui.copy(loadProgress = progress / 100f, chrome = it.ui.chrome.copy(isLoading = true)) } } } @@ -762,9 +746,7 @@ class NappletHostActivity : ComponentActivity() { request: WebResourceRequest, message: String, ) { - val panel = consolePanel ?: return - panel.appendLog(ConsoleMessage.MessageLevel.ERROR, message, request.url?.toString().orEmpty(), 0) - controlSheet?.updateConsoleCount(panel.entryCount) + chrome?.appendConsole(ConsoleLine(ConsoleLine.Level.ERROR, message, request.url?.toString().orEmpty(), 0)) } // ---- bridge: shell <-> native ---- @@ -937,63 +919,88 @@ class NappletHostActivity : ComponentActivity() { private fun barTitle(): String = title.ifBlank { getString(CommonsR.string.napplet_untitled) } /** - * The trusted top pull-down sheet: a small grabber at the top edge (out of the corner where the app - * shows its own avatar) that expands to the sandbox **shield**, the nSite network/Tor row (website - * mode only, taps through to the confirm dialog), reload, and the "what it can access" sheet. The - * applet can't draw over it. Mirrors the embedded tabs' Compose `TopControlSheet`. + * The trusted pull-down pill: a small grabber at the top edge (out of the corner where the app shows its + * own avatar) that expands to the sandbox **shield**, the nSite network/Tor row (website mode only, taps + * through to a relaunch), reload, find, text size and "what it can access". The applet can't draw over + * it. The same Compose components as the embedded tabs and the web browser. */ - private fun buildControlSheet(): View = - NappletControlSheet( - context = this, - initialState = - BrowserChrome.State( - surface = if (profile == HostProfile.WEBSITE) BrowserChrome.Surface.NSITE else BrowserChrome.Surface.NAPPLET, - presentation = BrowserChrome.Presentation.FULL_SCREEN, - url = "", - startUrl = "", - // Website-mode nSites can re-route over Tor; switching rebuilds the session, so the row - // taps through to a full relaunch rather than toggling inline. - torOn = if (profile.exposesNetwork && proxyPort > 0) useTor else null, - canFavorite = false, - hasAccessInfo = true, + private fun buildChrome(): BrowserChromeHost = + BrowserChromeHost( + activity = this, + dark = isDarkTheme(), + initial = + BrowserPillUi( + title = barTitle(), + chrome = + BrowserChrome.State( + surface = if (profile == HostProfile.WEBSITE) BrowserChrome.Surface.NSITE else BrowserChrome.Surface.NAPPLET, + presentation = BrowserChrome.Presentation.FULL_SCREEN, + url = "", + startUrl = "", + // Website-mode nSites can re-route over Tor; switching rebuilds the session, so the + // row taps through to a full relaunch rather than toggling inline. + torOn = if (profile.exposesNetwork && proxyPort > 0) useTor else null, + canFavorite = false, + hasAccessInfo = true, + ), ), - title = barTitle(), - listener = - object : NappletControlSheet.Listener { - override fun onAction(action: BrowserChrome.Action) { - val wv = if (this@NappletHostActivity::webView.isInitialized && !webViewGone) webView else null - when (action) { - BrowserChrome.Action.RELOAD -> wv?.reload() - BrowserChrome.Action.STOP -> wv?.stopLoading() - BrowserChrome.Action.FIND_IN_PAGE -> { - setConsoleShowing(false) - findBar?.show() - syncBackState() - } + listener = chromeListener, + ) + + private fun isDarkTheme(): Boolean = + when (themeType) { + "DARK" -> true + "LIGHT" -> false + else -> (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES + } + + private fun liveWebView(): WebView? = if (this::webView.isInitialized && !webViewGone) webView else null + + private val chromeListener = + object : BrowserChromeHost.Listener { + override fun onPillEvent(event: BrowserPillEvent) { + when (event) { + is BrowserPillEvent.Action -> + when (event.action) { + BrowserChrome.Action.RELOAD -> liveWebView()?.reload() + BrowserChrome.Action.STOP -> liveWebView()?.stopLoading() BrowserChrome.Action.TOR -> setNetworkMode(!useTor) BrowserChrome.Action.ACCESS_INFO -> showAccessDialog() BrowserChrome.Action.SITE_SETTINGS -> openPermissions() - BrowserChrome.Action.CONSOLE -> setConsoleShowing(!consoleShowing) else -> Unit } + is BrowserPillEvent.TextZoom -> { + liveWebView()?.let { BrowserWebTools.setTextZoom(it, event.percent) } + chrome?.let { it.ui = it.ui.copy(textZoom = event.percent) } } + BrowserPillEvent.PageInfo -> showAccessDialog() + BrowserPillEvent.Close -> finish() + else -> Unit + } + } - override fun onTextZoom(percent: Int) { - if (this@NappletHostActivity::webView.isInitialized && !webViewGone) BrowserWebTools.setTextZoom(webView, percent) - } + override fun onFind(query: String) { + val wv = liveWebView() ?: return + if (query.isEmpty()) wv.clearMatches() else wv.findAllAsync(query) + } - override fun onOriginTap() = showAccessDialog() + override fun onFindNext(forward: Boolean) { + liveWebView()?.findNext(forward) + } - override fun onClose() = finish() - }, - ).also { controlSheet = it } + override fun onFindClosed() { + liveWebView()?.clearMatches() + } - private fun setConsoleShowing(showing: Boolean) { - consoleShowing = showing - if (showing) findBar?.hide() - consolePanel?.setShowing(showing) - controlSheet?.setConsoleShowing(showing) - } + override fun onPermissionChange( + permission: BrowserSitePermission, + decision: BrowserSitePermission.Decision, + ) = Unit + + override fun onClearSiteData() = Unit + + override fun onPanelsChanged() = syncBackState() + } /** * Ask the broker to open this napplet's editable permission screen. The sandbox can't state its own @@ -1008,12 +1015,6 @@ class NappletHostActivity : ComponentActivity() { if (brokerMessenger != null) sendToBroker(msg) } - private fun buildConsolePanel(): View = - NappletConsolePanel(this).also { - it.onClearCallback = { controlSheet?.updateConsoleCount(0) } - consolePanel = it - } - /** * A thin determinate progress bar pinned to the top edge, like a browser's. Driven by * [NappletWebChromeClient.onProgressChanged]: visible while the shell + verified blobs load and gone From 6d2c37c3460ab1b0339df6d7b759e06a8afa6365 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 20:19:55 +0000 Subject: [PATCH 11/60] refactor(quartz): rename event classes to match their NIP names Old class names drifted from the NIP wording (or described the wrong kind). Every old name stays available as a @Deprecated typealias with a ReplaceWith, so external Quartz users get an IDE quick-fix instead of a break. Wire format is unchanged: parsing goes by kind. Renames: - FollowListEvent (39089) -> StarterPackEvent - LnZapPaymentRequestEvent/ResponseEvent (NIP-47) -> NwcRequestEvent/NwcResponseEvent - VideoHorizontalEvent/VideoVerticalEvent -> AddressableNormalVideoEvent/AddressableShortVideoEvent - CalendarEvent (31924) -> CalendarCollectionEvent - HashtagListEvent -> InterestListEvent - ContactCardEvent (30382) -> UserAssertionEvent - LnZapEvent/LnZapRequestEvent/LnZapEventInterface -> ZapReceiptEvent/ZapRequestEvent/ZapReceiptEventInterface - PrivateDmEvent -> EncryptedDmEvent, DeletionEvent -> DeletionRequestEvent - LongTextNoteEvent -> LongFormContentEvent, WikiNoteEvent -> WikiArticleEvent - PeopleListEvent -> FollowSetEvent, LabeledBookmarkListEvent -> BookmarkSetEvent - RelayFeedsListEvent -> FavoriteRelayListEvent, EmojiPackSelectionEvent -> EmojiListEvent - ChannelListEvent -> PublicChatListEvent, ChatMessageRelayListEvent -> DmRelayListEvent - SealedRumorEvent -> SealEvent, FileHeaderEvent -> FileMetadataEvent - GoalEvent -> ZapGoalEvent, StatusEvent -> UserStatusEvent - NIP90*Event -> Dvm*Event (all NIP-90 events) - NIP-29 group events get the Group prefix to separate them from NIP-43: PutUser, RemoveUser, EditMetadata, DeleteEvent, CreateInvite, UpdatePinList, JoinRequest, LeaveRequest; SupportedRolesEvent -> GroupRolesEvent Merged duplicate classes for the same kind: - nip61 TokenEvent (7375) was unreachable in EventFactory; now an alias of CashuTokenEvent. - nip61 NutzapRedemptionEvent (7376) was unreachable; CashuSpendingHistoryEvent now carries its hint providers and redeemedNutzaps(), plus CashuSpendingHistoryEvent.buildNutzapRedemption. - NIP-82 SoftwareReleaseEvent (30063) was never built by EventFactory; ReleaseArtifactSetEvent now exposes appId/version/channel/assets/releaseNotes and buildSoftwareRelease, and the app no longer re-parses the event. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015VvJ8XpeYqBe8bYT3bJ7UD --- .claude/skills/auth-signers/SKILL.md | 2 +- .../skills/nip85-trusted-assertions/SKILL.md | 12 +- .../references/event-hierarchy.md | 18 +- .../nostr-expert/references/nip-catalog.md | 12 +- .claude/skills/quartz-integration/SKILL.md | 22 +- .../references/searchable-kinds.md | 40 +-- .../amethyst/LocalPreferences.kt | 16 +- .../vitorpamplona/amethyst/model/Account.kt | 46 +-- .../model/AccountRelayGroupActions.kt | 32 +- .../amethyst/model/AccountSettings.kt | 36 +-- .../amethyst/model/AccountSyncedSettings.kt | 4 +- .../model/AccountSyncedSettingsInternal.kt | 4 +- .../amethyst/model/AccountZapActions.kt | 20 +- .../amethyst/model/EventBroadcaster.kt | 14 +- .../FavoriteAlgoFeedsOrchestrator.kt | 14 +- .../model/nip17Dms/DmRelayListState.kt | 16 +- .../nip30CustomEmojis/OwnedEmojiPacksState.kt | 8 +- .../nip47WalletConnect/NwcSignerState.kt | 18 +- .../hashtagLists/HashtagListState.kt | 26 +- .../interestSets/InterestSetsState.kt | 8 +- .../LabeledBookmarkListsState.kt | 42 +-- .../nip51Lists/peopleList/FollowListsState.kt | 26 +- .../nip51Lists/peopleList/PeopleListsState.kt | 42 +-- .../relayFeeds/RelayFeedListState.kt | 20 +- .../model/nip60Cashu/CashuWalletState.kt | 8 +- .../amethyst/napplet/NappletConsentSummary.kt | 4 +- .../napplet/gateways/AccountIdentityReader.kt | 4 +- .../amethyst/service/V4VPaymentHandler.kt | 6 +- .../amethyst/service/ZapPaymentHandler.kt | 22 +- .../service/lnurl/LightningAddressResolver.kt | 8 +- .../EventNotificationConsumer.kt | 26 +- .../notifications/NotificationContent.kt | 10 +- .../notifications/NotificationDispatcher.kt | 24 +- .../renderers/DirectMessageNotification.kt | 4 +- .../renderers/ZapNotification.kt | 8 +- .../FilterAccountInfoAndListsFromKey.kt | 16 +- .../NWCFinderFilterAssemblerSubscription.kt | 4 +- .../reqCommand/user/UserObservers.kt | 10 +- .../resourceusage/MeteringNostrSigner.kt | 4 +- .../uploads/hls/HlsVideoEventBuilder.kt | 14 +- .../markdown/RenderContentAsMarkdown.kt | 4 +- .../amethyst/ui/dal/FilterByListParams.kt | 4 +- .../amethyst/ui/feeds/PrefetchFeedMedia.kt | 4 +- .../ui/navigation/routes/RouteMaker.kt | 18 +- .../ui/note/NIP05VerificationDisplay.kt | 8 +- .../amethyst/ui/note/NoteCompose.kt | 64 ++-- .../amethyst/ui/note/NoteQuickActionMenu.kt | 8 +- .../ui/note/UpdateReactionTypeDialog.kt | 4 +- .../amethyst/ui/note/UpdateZapAmountDialog.kt | 10 +- .../ui/note/UpdateZapAmountViewModel.kt | 4 +- .../amethyst/ui/note/ZapCustomDialog.kt | 22 +- .../amethyst/ui/note/ZapPollNote.kt | 4 +- .../amethyst/ui/note/ZapPollNoteViewModel.kt | 12 +- .../WatchAndLoadMyEmojiList.kt | 6 +- .../note/elements/AddInboxRelayForDMCard.kt | 10 +- .../ui/note/elements/NoteActionSections.kt | 10 +- .../nip22Comments/CommentPostViewModel.kt | 10 +- .../amethyst/ui/note/share/SharedNoteCard.kt | 6 +- .../ui/note/types/CalendarCollectionRender.kt | 4 +- .../amethyst/ui/note/types/FileHeader.kt | 10 +- .../amethyst/ui/note/types/FollowList.kt | 6 +- .../amethyst/ui/note/types/Goal.kt | 6 +- .../amethyst/ui/note/types/LongForm.kt | 6 +- .../types/NIP90ContentDiscoveryResponse.kt | 4 +- .../amethyst/ui/note/types/NIP90Status.kt | 4 +- .../amethyst/ui/note/types/PeopleList.kt | 4 +- .../amethyst/ui/note/types/PrivateMessage.kt | 4 +- .../amethyst/ui/note/types/Publication.kt | 8 +- .../ui/note/types/PublicationSection.kt | 6 +- .../amethyst/ui/note/types/RelayList.kt | 4 +- .../amethyst/ui/note/types/SoftwareApp.kt | 48 ++- .../amethyst/ui/note/types/Text.kt | 4 +- .../amethyst/ui/note/types/Wiki.kt | 6 +- .../ui/note/types/WikiCollaboration.kt | 6 +- .../amethyst/ui/note/types/ZapEvent.kt | 4 +- .../ui/note/types/lists/BookmarkLists.kt | 4 +- .../amethyst/ui/note/types/lists/TagLists.kt | 6 +- .../amethyst/ui/screen/TopNavFilterState.kt | 8 +- .../ui/screen/loggedIn/AccountViewModel.kt | 68 ++-- .../loggedIn/DecryptAndIndexProcessor.kt | 34 +- .../loggedIn/articles/ArticlesFeedLoaded.kt | 4 +- .../loggedIn/articles/ArticlesTopBar.kt | 4 +- .../articles/dal/ArticlesFeedFilter.kt | 8 +- .../backups/BackupConflictListViews.kt | 16 +- .../display/BookmarkGroupItemOptions.kt | 4 +- .../calendars/CalendarCollectionsTopBar.kt | 4 +- .../calendars/CalendarCollectionsView.kt | 6 +- .../loggedIn/calendars/CalendarsViewModel.kt | 8 +- .../create/NewCalendarCollectionViewModel.kt | 12 +- .../dal/CalendarCollectionsFeedFilter.kt | 6 +- .../calendars/detail/AddToCalendarSheet.kt | 14 +- .../detail/CalendarEventDetailScreen.kt | 12 +- .../loggedIn/chats/feed/ChatMessageCompose.kt | 12 +- .../screen/loggedIn/chats/feed/ChatTimeAgo.kt | 6 +- .../chats/feed/layouts/ChatGroupPosition.kt | 4 +- .../chats/feed/types/RenderChatZap.kt | 4 +- .../chats/geohashChat/GeohashChatViewModel.kt | 4 +- .../loggedIn/chats/privateDM/ChatroomView.kt | 4 +- .../privateDM/datasource/FilterNip04DMs.kt | 6 +- .../privateDM/send/ChatNewMessageViewModel.kt | 10 +- .../header/LiveStreamGoalHeader.kt | 4 +- .../rooms/dal/ChatroomListKnownFeedFilter.kt | 6 +- .../rooms/dal/ChatroomListNewFeedFilter.kt | 6 +- .../rooms/datasource/FilterNip04DMsFromMe.kt | 4 +- .../rooms/datasource/FilterNip04DMsToMe.kt | 4 +- .../chats/rooms/feed/ChatroomListFeedView.kt | 6 +- .../loggedIn/discover/ChannelCardCompose.kt | 12 +- .../loggedIn/discover/DiscoverScreen.kt | 8 +- .../DiscoverLongFormFeedFilter.kt | 8 +- .../discover/nip23LongForm/LongFormCard.kt | 4 +- .../nip23LongForm/LongFormPostViewModel.kt | 10 +- .../DiscoverFollowSetsFeedFilter.kt | 8 +- .../discover/nip51FollowSets/FollowSetCard.kt | 6 +- .../dvms/DvmContentDiscoveryScreen.kt | 18 +- .../loggedIn/dvms/FavoriteAlgoFeedToggle.kt | 4 +- .../NIP90ContentDiscoveryResponseFilter.kt | 8 +- .../membershipManagement/MyEmojiListScreen.kt | 4 +- .../followPacks/feed/FollowPackFeedScreen.kt | 6 +- .../FollowPackFeedConversationsFeedFilter.kt | 4 +- .../dal/FollowPackFeedNewThreadFeedFilter.kt | 16 +- .../feed/dal/FollowPackMembersFeedFilter.kt | 6 +- .../FollowPackFeedFilterSubAssembler.kt | 4 +- .../followPacks/list/FollowPacksFeedLoaded.kt | 4 +- .../followPacks/list/FollowPacksTopBar.kt | 4 +- .../loggedIn/geohash/dal/GeoHashFeedFilter.kt | 12 +- .../loggedIn/hashtag/dal/HashtagFeedFilter.kt | 12 +- .../datasource/FilterPostsByHashtags.kt | 8 +- .../home/dal/HomeNewThreadFeedFilter.kt | 24 +- .../home/nip75Goals/NewGoalViewModel.kt | 4 +- .../display/lists/PeopleListViewModel.kt | 4 +- .../display/packs/FollowPackViewModel.kt | 4 +- .../ui/screen/loggedIn/longs/LongsTopBar.kt | 4 +- .../loggedIn/longs/dal/LongsFeedFilter.kt | 8 +- .../datasource/NestRoomFilterAssembler.kt | 4 +- .../room/lifecycle/NestRoomEventCollectors.kt | 8 +- .../notifications/CardFeedContentState.kt | 12 +- .../notifications/NotificationSummaryState.kt | 6 +- .../dal/NotificationFeedFilter.kt | 38 +-- .../loggedIn/podcasts/PodcastTopSupporters.kt | 4 +- .../dal/UserProfileGalleryFeedFilter.kt | 6 +- .../mutual/dal/UserProfileMutualFeedFilter.kt | 8 +- .../dal/UserProfileNewThreadFeedFilter.kt | 8 +- .../profile/payment/SendPaymentContent.kt | 30 +- .../profile/payment/SendPaymentScreen.kt | 14 +- .../profile/relays/RelayFeedViewModel.kt | 6 +- .../zaps/dal/UserProfileZapsViewModel.kt | 8 +- .../screen/loggedIn/relays/KindDisplayName.kt | 116 +++---- .../loggedIn/relays/RelayInformationScreen.kt | 4 +- .../relays/dm/DMRelayListViewModel.kt | 4 +- .../ui/screen/loggedIn/shorts/ShortsTopBar.kt | 4 +- .../loggedIn/shorts/dal/ShortsFeedFilter.kt | 8 +- .../loggedIn/threadview/ThreadFeedView.kt | 48 +-- .../loggedIn/video/FileHeaderCardCompose.kt | 8 +- .../ui/screen/loggedIn/video/StoriesTopBar.kt | 6 +- .../ui/screen/loggedIn/video/VideoScreen.kt | 4 +- .../loggedIn/video/dal/VideoFeedFilter.kt | 22 +- .../appfunctions/AmethystAppFunctions.kt | 18 +- .../calendar/CalendarsViewModelFlowTest.kt | 6 +- .../model/AddressedAuthorRoutingTest.kt | 4 +- .../model/BuzzWorkspaceChannelTest.kt | 4 +- .../amethyst/model/HomeFeedTypeTest.kt | 8 +- .../FavoriteAlgoFeedTopNavFilterTest.kt | 4 +- .../service/okhttp/DelayingTestSigner.kt | 4 +- .../FilterGroupNotificationsToPubkeyTest.kt | 2 +- .../resourceusage/ResourceUsageLedgerTest.kt | 4 +- .../uploads/hls/HlsVideoEventBuilderTest.kt | 8 +- .../amethyst/ui/RouteForPointerTest.kt | 12 +- .../amethyst/ui/note/share/QrPayloadTest.kt | 6 +- .../ui/note/share/SharedNoteCardTest.kt | 4 +- .../dal/NotificationTagsAnEventByUserTest.kt | 10 +- .../quartz/benchmark/GiftWrapBenchmark.kt | 6 +- .../benchmark/GiftWrapReceivingBenchmark.kt | 8 +- .../benchmark/GiftWrapSigningBenchmark.kt | 8 +- .../benchmark/LargeDBInsertBenchmark.kt | 4 +- cli/ROADMAP.md | 2 +- .../com/vitorpamplona/amethyst/cli/Context.kt | 8 +- .../amethyst/cli/commands/GiftCommands.kt | 8 +- .../amethyst/cli/commands/GrapeRankCommand.kt | 2 +- .../amethyst/cli/commands/RelayCommands.kt | 14 +- .../cli/commands/RelayGroupCommands.kt | 12 +- .../commands/RelayGroupModerationCommands.kt | 16 +- .../amethyst/cli/commands/StatusReport.kt | 8 +- .../amethyst/cli/commands/ZapCommand.kt | 18 +- .../commands/graperank/GrapeRankOperator.kt | 8 +- .../commands/graperank/GrapeRankPublish.kt | 10 +- .../cli/commands/graperank/GrapeRankScore.kt | 6 +- .../commons/account/AccountBootstrapEvents.kt | 18 +- .../amethyst/commons/actions/ZapActions.kt | 24 +- .../signers/NostrSignerPermissionLedger.kt | 28 +- .../commons/feeds/FeedContentState.kt | 4 +- .../amethyst/commons/marmot/MarmotManager.kt | 8 +- .../amethyst/commons/model/DvmHeartbeat.kt | 4 +- .../amethyst/commons/model/HomeFeedType.kt | 16 +- .../amethyst/commons/model/IAccount.kt | 12 +- .../amethyst/commons/model/Note.kt | 28 +- .../amethyst/commons/model/ThreadAssembler.kt | 4 +- .../amethyst/commons/model/User.kt | 6 +- .../backups/ReplaceableBackupConflict.kt | 16 +- .../commons/model/cache/CachePruner.kt | 22 +- .../commons/model/cache/EventCache.kt | 200 ++++++------ .../nip23LongContent/LongFormPublishAction.kt | 8 +- .../PublicChatListDecryptionCache.kt | 12 +- .../nip28PublicChats/PublicChatListState.kt | 30 +- .../nip29RelayGroups/RelayGroupChannel.kt | 4 +- .../model/nip30CustomEmojis/EmojiPackState.kt | 20 +- .../nip38UserStatuses/UserStatusAction.kt | 20 +- .../blockPeopleList/BlockPeopleListState.kt | 34 +- .../HashtagListDecryptionCache.kt | 8 +- .../peopleList/PeopleListDecryptionCache.kt | 28 +- .../RelayFeedsListDecryptionCache.kt | 4 +- .../ContactCardDecryptionCache.kt | 12 +- .../ContactCardsState.kt | 18 +- .../nip85TrustedAssertions/UserCardsCache.kt | 6 +- .../commons/model/privateChats/ChatPreview.kt | 14 +- .../commons/model/privateChats/Chatroom.kt | 6 +- .../model/topNavFeeds/UsingRelayUnwrapper.kt | 8 +- .../topNavFeeds/noteBased/NoteFeedFlow.kt | 38 +-- .../commons/moderation/HashtagSpamCheck.kt | 4 +- .../notifications/NotificationKinds.kt | 10 +- .../commons/napplet/NappletRelayCleartext.kt | 4 +- .../FilterBasicAccountInfoFromKeys.kt | 12 +- .../FilterBookmarksAndReportsFromKey.kt | 4 +- .../metadata/FilterFollowsAndMutesFromKey.kt | 16 +- .../FilterNotificationsToPubkey.kt | 10 +- .../assemblers/ContactCardFilters.kt | 4 +- .../relayClient/calendars/CalendarKinds.kt | 4 +- .../channel/FilterGoalForLiveActivity.kt | 8 +- .../channel/FilterMessagesToLiveStream.kt | 4 +- .../relayGroup/RelayGroupFilterBuilders.kt | 12 +- .../communities/FilterCommunityPosts.kt | 4 +- .../diagnostics/DmRelayDiagnosticsLogger.kt | 4 +- .../FilterLongFormByAllCommunities.kt | 6 +- .../nip23LongForm/FilterLongFormByAuthors.kt | 4 +- .../FilterLongFormByCommunity.kt | 6 +- .../nip23LongForm/FilterLongFormByGeohash.kt | 4 +- .../nip23LongForm/FilterLongFormByHashtag.kt | 4 +- .../nip23LongForm/FilterLongFormGlobal.kt | 4 +- .../FilterFollowSetsByAllCommunities.kt | 6 +- .../FilterFollowSetsByAuthors.kt | 4 +- .../FilterFollowSetsByCommunity.kt | 6 +- .../FilterFollowSetsByGeohash.kt | 4 +- .../FilterFollowSetsByHashtag.kt | 4 +- .../nip51FollowSets/FilterFollowSetsGlobal.kt | 4 +- .../FilterRepliesAndReactionsToAddresses.kt | 8 +- .../FilterRepliesAndReactionsToNotes.kt | 16 +- .../geohash/FilterPostsByGeohash.kt | 8 +- .../nip01Core/FilterHomePostsByGeohashes.kt | 16 +- .../nip01Core/FilterHomePostsByHashtags.kt | 16 +- .../nip65Follows/FilterHomePostsByAuthors.kt | 16 +- .../FilterHomePostsFromCommunities.kt | 16 +- .../FilterHomePostsByAlgoFeedIds.kt | 8 +- .../longs/FilterLongsByAllCommunities.kt | 6 +- .../relayClient/longs/FilterLongsByAuthors.kt | 4 +- .../longs/FilterLongsByGeohashes.kt | 4 +- .../relayClient/longs/FilterLongsByHashtag.kt | 4 +- .../relayClient/longs/FilterLongsGlobal.kt | 4 +- .../relayClient/nip17Dm/GiftWrapDecryptor.kt | 8 +- .../FilterNWCPaymentsFromRequests.kt | 4 +- .../profile/FilterUserProfileLists.kt | 12 +- .../profile/FilterUserProfileMedia.kt | 8 +- .../profile/FilterUserProfilePosts.kt | 8 +- .../profile/FilterUserProfileZapReceived.kt | 4 +- .../relayClient/relay/FilterPostsByRelay.kt | 8 +- .../shorts/FilterShortsByAllCommunities.kt | 6 +- .../shorts/FilterShortsByAuthors.kt | 4 +- .../shorts/FilterShortsByGeohashes.kt | 4 +- .../shorts/FilterShortsByHashtag.kt | 4 +- .../relayClient/shorts/FilterShortsGlobal.kt | 4 +- .../FilterSoftwareAppsByAuthors.kt | 4 +- .../FilterSoftwareAppsByHashtag.kt | 4 +- .../softwareapps/FilterSoftwareAppsGlobal.kt | 4 +- .../user/watchers/FilterUserMetadataForKey.kt | 8 +- .../commons/relayClient/video/FeedBasis.kt | 18 +- .../FilterPictureAndVideoByAllCommunities.kt | 4 +- .../amethyst/commons/search/KindRegistry.kt | 32 +- .../commons/search/RenderableKinds.kt | 44 +-- .../commons/search/SearchResultSorter.kt | 4 +- .../commons/service/nwc/NwcPaymentTracker.kt | 8 +- .../amethyst/commons/service/pow/PoWPolicy.kt | 8 +- .../LiveStreamTopZappersViewModel.kt | 12 +- .../commons/viewmodels/RoomZapsState.kt | 8 +- .../commons/actions/PrivateNoteFactoryTest.kt | 8 +- .../commons/actions/ZapActionsTest.kt | 8 +- .../commons/model/ThreadAssemblerTest.kt | 4 +- .../model/privateChats/ChatPreviewTest.kt | 4 +- .../notifications/NotificationKindsTest.kt | 16 +- .../commons/napplet/NappletBrokerTest.kt | 4 +- .../commons/onchain/OnchainZapSenderTest.kt | 4 +- .../channel/FilterRelayGroupStateTest.kt | 4 +- .../RelayGroupFilterBuildersTest.kt | 10 +- .../home/HomeMediaKindCoverageTest.kt | 8 +- .../FilterHomePostsByAlgoFeedIdsTest.kt | 6 +- .../commons/search/SearchResultSorterTest.kt | 6 +- .../amethyst/commons/search/SearchSeedTest.kt | 6 +- .../commons/viewmodels/RoomZapsStateTest.kt | 6 +- .../commons/cashu/ops/CashuWalletOps.kt | 34 +- .../commons/model/nip52Calendar/IcsExport.kt | 6 +- .../commons/model/nip57Zaps/ZapAction.kt | 12 +- .../service/lnurl/LightningAddressResolver.kt | 6 +- .../lnurl/OkHttpLnurlEndpointResolver.kt | 2 +- .../commons/viewmodels/NestViewModel.kt | 2 +- .../marmot/MarmotEditsAndSystemRowsTest.kt | 6 +- .../model/nip52Calendar/IcsExportTest.kt | 4 +- .../nip71Video/DivineVideoInteropTest.kt | 20 +- .../nip71Video/VideoTrackSelectionTest.kt | 4 +- .../nip17Dm/DmInboxRelayResolverOutboxTest.kt | 10 +- .../commons/viewmodels/NestViewModelTest.kt | 4 +- .../commons/nip17Dm/ui/IncognitoBadge.kt | 4 +- .../commons/ui/note/CalendarCollectionCard.kt | 4 +- .../vitorpamplona/amethyst/desktop/Main.kt | 10 +- .../desktop/cache/DesktopLocalCache.kt | 50 +-- .../desktop/feeds/DesktopFeedFilters.kt | 10 +- .../desktop/followpacks/BulkFollowAction.kt | 6 +- .../desktop/followpacks/FollowPackEditor.kt | 8 +- .../desktop/followpacks/FollowPacksState.kt | 18 +- .../desktop/followpacks/ui/DiscoverScreen.kt | 4 +- .../followpacks/ui/FollowAllConfirmDialog.kt | 4 +- .../ui/FollowPackBrowseAllScreen.kt | 6 +- .../followpacks/ui/FollowPackComponents.kt | 6 +- .../followpacks/ui/FollowPackDetailScreen.kt | 6 +- .../followpacks/ui/RenderFollowPackCard.kt | 4 +- .../desktop/model/DesktopAccountRelays.kt | 12 +- .../desktop/model/DesktopDmRelayState.kt | 2 +- .../desktop/model/DesktopHiddenUsersState.kt | 10 +- .../amethyst/desktop/model/DesktopIAccount.kt | 12 +- .../desktop/model/DesktopRelayListMutator.kt | 4 +- .../amethyst/desktop/nwc/NwcPaymentHandler.kt | 14 +- .../DesktopRelaySubscriptionsCoordinator.kt | 2 +- .../desktop/subscriptions/FilterBuilders.kt | 14 +- .../desktop/subscriptions/FilterDMs.kt | 10 +- .../desktop/ui/ArticleReaderScreen.kt | 10 +- .../amethyst/desktop/ui/NoteActions.kt | 8 +- .../desktop/ui/NotificationsScreen.kt | 10 +- .../amethyst/desktop/ui/ReadsScreen.kt | 16 +- .../amethyst/desktop/ui/UserProfileScreen.kt | 26 +- .../amethyst/desktop/ui/chats/ChatPane.kt | 10 +- .../desktop/ui/chats/ChatroomListState.kt | 4 +- .../desktop/ui/chats/composer/GifResult.kt | 6 +- .../desktop/ui/note/DesktopRichText.kt | 2 +- .../DesktopNotificationAutoDispatcher.kt | 12 +- .../desktop/ui/relay/DmRelayEditor.kt | 4 +- .../desktop/ui/search/SearchResultsList.kt | 6 +- .../cache/DesktopDmRelayListConsumeTest.kt | 4 +- .../desktop/nwc/NwcPaymentHandlerTest.kt | 6 +- .../desktop/nwc/NwcRpcIntegrationTest.kt | 18 +- .../geode/ConcordPlaneKeyDeletionTest.kt | 6 +- .../geode/DeletionSettleBenchmark.kt | 4 +- .../vitorpamplona/geode/DeletionSyncTest.kt | 18 +- .../geode/Nip01ComplianceTest.kt | 12 +- .../vitorpamplona/geode/Nip09DeletionTest.kt | 12 +- quartz/CLIENT.md | 4 +- .../quartz/nip01Core/EventSigCheck.kt | 4 +- .../LnZapPaymentRequestNip44EventTest.kt | 6 +- .../quartz/nip57Zaps/PrivateZapTests.kt | 8 +- .../nip59GiftWraps/GiftWrapEventTest.kt | 46 +-- .../intents/requests/DecryptZapRequest.kt | 4 +- .../client/NostrSignerExternal.kt | 4 +- .../handlers/BackgroundRequestHandler.kt | 4 +- .../handlers/ForegroundRequestHandler.kt | 4 +- .../com/vitorpamplona/quartz/buzz/README.md | 2 +- .../buzz/moderation/ModerationUnbanEvent.kt | 6 +- .../cord05Invites/ConcordDirectInvite.kt | 6 +- .../assistant/CurationCopy.kt | 4 +- .../graperank/GrapeRankCrawler.kt | 4 +- .../graperank/GrapeRankPublisher.kt | 26 +- .../release/Nip82ReleaseDetector.kt | 10 +- .../release/SoftwareReleaseEvent.kt | 94 +----- .../release/TagArrayBuilderExt.kt | 11 +- .../publications/PublicationSectionRef.kt | 8 +- .../vitorpamplona/quartz/kinds/KindNames.kt | 284 ++++++++--------- .../quartz/marmot/MarmotWelcomeSender.kt | 2 +- .../quartz/marmot/RecipientRelayFetcher.kt | 8 +- .../KeyPackageRelayListEvent.kt | 2 +- .../marmot/mip02Welcome/WelcomeGiftWrap.kt | 8 +- .../cache/projection/EventStoreProjection.kt | 6 +- .../NostrClientNegentropyDeletionSettleExt.kt | 4 +- .../quartz/nip01Core/signers/NostrSigner.kt | 4 +- .../nip01Core/signers/NostrSignerInternal.kt | 4 +- .../nip01Core/signers/NostrSignerSync.kt | 6 +- .../nip01Core/store/EventStoreDeletionsExt.kt | 8 +- .../store/sqlite/DeletionRequestModule.kt | 4 +- .../store/sqlite/SQLiteEventStore.kt | 4 +- .../quartz/nip04Dm/PrivateDMCache.kt | 14 +- ...{PrivateDmEvent.kt => EncryptedDmEvent.kt} | 14 +- .../nip04Dm/messages/TagArrayBuilderExt.kt | 4 +- .../quartz/nip09Deletions/DeletionIndex.kt | 8 +- ...letionEvent.kt => DeletionRequestEvent.kt} | 14 +- .../quartz/nip10Notes/BaseThreadedEvent.kt | 4 +- .../quartz/nip13Pow/signer/PoWNostrSigner.kt | 4 +- .../quartz/nip17Dm/NIP17Factory.kt | 10 +- ...eRelayListEvent.kt => DmRelayListEvent.kt} | 18 +- ...xtNoteEvent.kt => LongFormContentEvent.kt} | 10 +- .../nip23LongContent/TagArrayBuilderExt.kt | 8 +- ...nelListEvent.kt => PublicChatListEvent.kt} | 32 +- .../list/TagArrayBuilderExt.kt | 4 +- .../metadata/GroupPinnedEvent.kt | 2 +- ...portedRolesEvent.kt => GroupRolesEvent.kt} | 10 +- ...viteEvent.kt => GroupCreateInviteEvent.kt} | 10 +- ...EventEvent.kt => GroupDeleteEventEvent.kt} | 10 +- ...dataEvent.kt => GroupEditMetadataEvent.kt} | 10 +- .../{PutUserEvent.kt => GroupPutUserEvent.kt} | 10 +- ...veUserEvent.kt => GroupRemoveUserEvent.kt} | 10 +- ...istEvent.kt => GroupUpdatePinListEvent.kt} | 10 +- ...questEvent.kt => GroupJoinRequestEvent.kt} | 10 +- ...uestEvent.kt => GroupLeaveRequestEvent.kt} | 10 +- ...ackSelectionEvent.kt => EmojiListEvent.kt} | 18 +- .../selection/TagArrayBuilderExt.kt | 6 +- .../{StatusEvent.kt => UserStatusEvent.kt} | 18 +- .../signer/NostrSignerRemote.kt | 4 +- .../quartz/nip47WalletConnect/Nip47Client.kt | 40 +-- .../quartz/nip47WalletConnect/Nip47Server.kt | 54 ++-- .../quartz/nip47WalletConnect/README.md | 16 +- .../cache/NostrWalletConnectRequestCache.kt | 14 +- .../cache/NostrWalletConnectResponseCache.kt | 14 +- ...mentRequestEvent.kt => NwcRequestEvent.kt} | 12 +- ...ntResponseEvent.kt => NwcResponseEvent.kt} | 12 +- .../rpc/NwcTransactionMetadata.kt | 2 +- .../nip50Search/SearchFieldExtractor.kt | 40 +-- .../quartz/nip50Search/SearchableKinds.kt | 40 +-- ...FollowListEvent.kt => StarterPackEvent.kt} | 26 +- .../followList/TagArrayBuilderExt.kt | 16 +- ...shtagListEvent.kt => InterestListEvent.kt} | 32 +- .../hashtagList/TagArrayBuilderExt.kt | 4 +- ...okmarkListEvent.kt => BookmarkSetEvent.kt} | 36 ++- .../labeledBookmarkList/TagArrayBuilderExt.kt | 10 +- .../{PeopleListEvent.kt => FollowSetEvent.kt} | 56 ++-- .../peopleList/TagArrayBuilderExt.kt | 10 +- ...ListEvent.kt => FavoriteRelayListEvent.kt} | 22 +- .../relayLists/tags/TagArrayBuilderExt.kt | 4 +- .../ReleaseArtifactSetEvent.kt | 57 +++- ...darEvent.kt => CalendarCollectionEvent.kt} | 10 +- .../calendar/TagArrayBuilderExt.kt | 4 +- .../quartz/nip54Wiki/TagArrayBuilderExt.kt | 8 +- .../{WikiNoteEvent.kt => WikiArticleEvent.kt} | 12 +- .../nip57Zaps/IPrivateZapsDecryptionCache.kt | 4 +- .../quartz/nip57Zaps/PrivateZapCache.kt | 14 +- .../nip57Zaps/PrivateZapRequestBuilder.kt | 2 +- .../{LnZapEvent.kt => ZapReceiptEvent.kt} | 22 +- ...terface.kt => ZapReceiptEventInterface.kt} | 8 +- ...nZapRequestEvent.kt => ZapRequestEvent.kt} | 32 +- .../validate/LnZapReceiptValidator.kt | 10 +- .../quartz/nip59Giftwrap/HasInnerEvent.kt | 2 +- .../quartz/nip59Giftwrap/rumors/Rumor.kt | 4 +- .../{SealedRumorEvent.kt => SealEvent.kt} | 16 +- .../history/CashuSpendingHistoryEvent.kt | 26 +- .../redemption/NutzapRedemptionEvent.kt | 69 ++--- .../redemption/TagArrayBuilderExt.kt | 5 +- .../quartz/nip61Nutzaps/token/TokenEvent.kt | 38 +-- ...vent.kt => AddressableNormalVideoEvent.kt} | 14 +- ...Event.kt => AddressableShortVideoEvent.kt} | 12 +- .../nip75ZapGoals/TagArrayBuilderExt.kt | 14 +- .../{GoalEvent.kt => ZapGoalEvent.kt} | 10 +- .../users/TagArrayBuilderExt.kt | 40 +-- ...tactCardEvent.kt => UserAssertionEvent.kt} | 24 +- .../clientTag/NostrSignerWithClientTag.kt | 4 +- ....kt => DvmContentDiscoveryRequestEvent.kt} | 10 +- .../TagArrayBuilderExt.kt | 6 +- ...kt => DvmContentDiscoveryResponseEvent.kt} | 12 +- ...ent.kt => DvmContentSearchRequestEvent.kt} | 10 +- ...nt.kt => DvmContentSearchResponseEvent.kt} | 12 +- ...tEvent.kt => DvmEventCountRequestEvent.kt} | 10 +- ...Event.kt => DvmEventCountResponseEvent.kt} | 10 +- ...t => DvmEventPowDelegationRequestEvent.kt} | 10 +- ... => DvmEventPowDelegationResponseEvent.kt} | 10 +- ...=> DvmEventPublishScheduleRequestEvent.kt} | 10 +- ...> DvmEventPublishScheduleResponseEvent.kt} | 10 +- ...kt => DvmEventTimestampingRequestEvent.kt} | 10 +- ...t => DvmEventTimestampingResponseEvent.kt} | 10 +- ...t.kt => DvmImageGenerationRequestEvent.kt} | 10 +- ....kt => DvmImageGenerationResponseEvent.kt} | 10 +- ...vent.kt => DvmImageToVideoRequestEvent.kt} | 10 +- ...ent.kt => DvmImageToVideoResponseEvent.kt} | 10 +- ...Event.kt => DvmMalwareScanRequestEvent.kt} | 10 +- ...vent.kt => DvmMalwareScanResponseEvent.kt} | 10 +- ...estEvent.kt => DvmOpReturnRequestEvent.kt} | 10 +- ...seEvent.kt => DvmOpReturnResponseEvent.kt} | 10 +- ...vent.kt => DvmPeopleSearchRequestEvent.kt} | 10 +- ...ent.kt => DvmPeopleSearchResponseEvent.kt} | 12 +- ...{NIP90StatusEvent.kt => DvmStatusEvent.kt} | 10 +- .../nip90Dvms/status/TagArrayBuilderExt.kt | 6 +- ...ent.kt => DvmSummarizationRequestEvent.kt} | 10 +- ...nt.kt => DvmSummarizationResponseEvent.kt} | 10 +- ...nt.kt => DvmTextExtractionRequestEvent.kt} | 10 +- ...t.kt => DvmTextExtractionResponseEvent.kt} | 10 +- ...nt.kt => DvmTextGenerationRequestEvent.kt} | 10 +- ...t.kt => DvmTextGenerationResponseEvent.kt} | 10 +- ...vent.kt => DvmTextToSpeechRequestEvent.kt} | 10 +- ...ent.kt => DvmTextToSpeechResponseEvent.kt} | 10 +- ...Event.kt => DvmTranslationRequestEvent.kt} | 10 +- ...vent.kt => DvmTranslationResponseEvent.kt} | 10 +- ...ent.kt => DvmUserDiscoveryRequestEvent.kt} | 10 +- ...nt.kt => DvmUserDiscoveryResponseEvent.kt} | 12 +- ...t.kt => DvmVideoConversionRequestEvent.kt} | 10 +- ....kt => DvmVideoConversionResponseEvent.kt} | 10 +- ....kt => DvmVideoTranslationRequestEvent.kt} | 10 +- ...kt => DvmVideoTranslationResponseEvent.kt} | 10 +- ...ileHeaderEvent.kt => FileMetadataEvent.kt} | 12 +- .../nip94FileMetadata/TagArrayBuilderExt.kt | 30 +- .../quartz/utils/EventFactory.kt | 292 +++++++++--------- .../moderation/ModerationUnbanEventTest.kt | 6 +- .../graperank/GrapeRankPublisherTest.kt | 20 +- .../SoftwareApplicationEventTest.kt | 57 +++- .../AssertionEventTest.kt | 8 +- .../ContactCardPetNameTest.kt | 24 +- .../projection/EventStoreProjectionTest.kt | 24 +- .../nip01Core/store/sqlite/AddressableTest.kt | 22 +- .../store/sqlite/AuthorsMissingOutboxTest.kt | 4 +- .../nip01Core/store/sqlite/BasicTest.kt | 4 +- .../nip01Core/store/sqlite/DeletionTest.kt | 48 +-- .../sqlite/InsertOutcomeClassificationTest.kt | 6 +- .../store/sqlite/QueryAssemblerTest.kt | 4 +- .../nip01Core/store/sqlite/SearchTest.kt | 14 +- .../ChannelSettingsTagTest.kt | 14 +- .../Nip29ArmadaInteropTest.kt | 48 +-- .../quartz/nip29RelayGroups/PinEventsTest.kt | 12 +- .../quartz/nip29RelayGroups/SubgroupTest.kt | 10 +- .../BunkerRequestProcessorConcurrencyTest.kt | 4 +- .../server/BunkerRequestProcessorTest.kt | 6 +- .../server/NostrConnectSignerServiceTest.kt | 4 +- .../LnZapPaymentRequestNip44EventTest.kt | 6 +- .../NwcOutgoingMetadataTest.kt | 2 +- .../NwcRefusalRoundTripTest.kt | 12 +- ...estEventTest.kt => NwcRequestEventTest.kt} | 16 +- .../nip50Search/SearchFieldExtractorTest.kt | 16 +- ...tTest.kt => FavoriteRelayListEventTest.kt} | 34 +- .../relayLists/RelayListPublicEntriesTest.kt | 4 +- .../nip52Calendar/CalendarHintProviderTest.kt | 4 +- .../quartz/nip54Wiki/WikiCollaborationTest.kt | 10 +- .../nip57Zaps/LnZapRequestAnonTagTest.kt | 14 +- .../nip57Zaps/LnZapRequestGroupTagTest.kt | 4 +- .../nip57Zaps/LnZapRequestKindTagTest.kt | 12 +- .../validate/LnZapReceiptValidatorTest.kt | 6 +- .../nip61Nutzaps/NutzapInfoEventTest.kt | 4 +- .../nip61Nutzaps/NutzapRedemptionTest.kt | 61 ++++ .../AddressableVideoEventAddressTest.kt | 14 +- .../quartz/marmot/MarmotMipBehaviorTest.kt | 6 +- .../nip01Core/jackson/JacksonMapperTest.kt | 4 +- .../KotlinSerializationMapperTest.kt | 10 +- .../RelayGroupFilterServingRelayTest.kt | 10 +- .../quartz/nip01Core/store/fs/FsEventStore.kt | 8 +- .../buzz/interop/BuzzRelayLiveInteropTest.kt | 8 +- .../store/fs/FsAuthorsMissingOutboxTest.kt | 4 +- .../nip01Core/store/fs/FsDeletionTest.kt | 62 ++-- .../quartz/nip01Core/store/fs/FsParityTest.kt | 28 +- .../quartz/nip01Core/store/fs/FsSearchTest.kt | 4 +- .../quartz/nip01Core/store/fs/FsSlotsTest.kt | 22 +- 546 files changed, 3851 insertions(+), 3343 deletions(-) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip04Dm/messages/{PrivateDmEvent.kt => EncryptedDmEvent.kt} (92%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip09Deletions/{DeletionEvent.kt => DeletionRequestEvent.kt} (90%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip17Dm/settings/{ChatMessageRelayListEvent.kt => DmRelayListEvent.kt} (86%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip23LongContent/{LongTextNoteEvent.kt => LongFormContentEvent.kt} (94%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/{ChannelListEvent.kt => PublicChatListEvent.kt} (89%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/metadata/{SupportedRolesEvent.kt => GroupRolesEvent.kt} (83%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/{CreateInviteEvent.kt => GroupCreateInviteEvent.kt} (82%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/{DeleteEventEvent.kt => GroupDeleteEventEvent.kt} (84%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/{EditMetadataEvent.kt => GroupEditMetadataEvent.kt} (92%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/{PutUserEvent.kt => GroupPutUserEvent.kt} (88%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/{RemoveUserEvent.kt => GroupRemoveUserEvent.kt} (84%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/{UpdatePinListEvent.kt => GroupUpdatePinListEvent.kt} (85%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/request/{JoinRequestEvent.kt => GroupJoinRequestEvent.kt} (84%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/request/{LeaveRequestEvent.kt => GroupLeaveRequestEvent.kt} (82%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/selection/{EmojiPackSelectionEvent.kt => EmojiListEvent.kt} (85%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip38UserStatus/{StatusEvent.kt => UserStatusEvent.kt} (90%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/events/{LnZapPaymentRequestEvent.kt => NwcRequestEvent.kt} (91%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/events/{LnZapPaymentResponseEvent.kt => NwcResponseEvent.kt} (90%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/{FollowListEvent.kt => StarterPackEvent.kt} (90%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/{HashtagListEvent.kt => InterestListEvent.kt} (90%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/{LabeledBookmarkListEvent.kt => BookmarkSetEvent.kt} (94%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/{PeopleListEvent.kt => FollowSetEvent.kt} (93%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/{RelayFeedsListEvent.kt => FavoriteRelayListEvent.kt} (88%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip52Calendar/calendar/{CalendarEvent.kt => CalendarCollectionEvent.kt} (89%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/{WikiNoteEvent.kt => WikiArticleEvent.kt} (95%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/{LnZapEvent.kt => ZapReceiptEvent.kt} (87%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/{LnZapEventInterface.kt => ZapReceiptEventInterface.kt} (82%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/{LnZapRequestEvent.kt => ZapRequestEvent.kt} (91%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/seals/{SealedRumorEvent.kt => SealEvent.kt} (93%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/{VideoHorizontalEvent.kt => AddressableNormalVideoEvent.kt} (82%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/{VideoVerticalEvent.kt => AddressableShortVideoEvent.kt} (83%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip75ZapGoals/{GoalEvent.kt => ZapGoalEvent.kt} (94%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip85TrustedAssertions/users/{ContactCardEvent.kt => UserAssertionEvent.kt} (89%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentDiscoveryRequest/{NIP90ContentDiscoveryRequestEvent.kt => DvmContentDiscoveryRequestEvent.kt} (85%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentDiscoveryResponse/{NIP90ContentDiscoveryResponseEvent.kt => DvmContentDiscoveryResponseEvent.kt} (82%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentSearch/{NIP90ContentSearchRequestEvent.kt => DvmContentSearchRequestEvent.kt} (89%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentSearch/{NIP90ContentSearchResponseEvent.kt => DvmContentSearchResponseEvent.kt} (83%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventCount/{NIP90EventCountRequestEvent.kt => DvmEventCountRequestEvent.kt} (86%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventCount/{NIP90EventCountResponseEvent.kt => DvmEventCountResponseEvent.kt} (82%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPowDelegation/{NIP90EventPowDelegationRequestEvent.kt => DvmEventPowDelegationRequestEvent.kt} (83%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPowDelegation/{NIP90EventPowDelegationResponseEvent.kt => DvmEventPowDelegationResponseEvent.kt} (79%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPublishSchedule/{NIP90EventPublishScheduleRequestEvent.kt => DvmEventPublishScheduleRequestEvent.kt} (84%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPublishSchedule/{NIP90EventPublishScheduleResponseEvent.kt => DvmEventPublishScheduleResponseEvent.kt} (79%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventTimestamping/{NIP90EventTimestampingRequestEvent.kt => DvmEventTimestampingRequestEvent.kt} (83%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventTimestamping/{NIP90EventTimestampingResponseEvent.kt => DvmEventTimestampingResponseEvent.kt} (80%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageGeneration/{NIP90ImageGenerationRequestEvent.kt => DvmImageGenerationRequestEvent.kt} (89%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageGeneration/{NIP90ImageGenerationResponseEvent.kt => DvmImageGenerationResponseEvent.kt} (80%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageToVideo/{NIP90ImageToVideoRequestEvent.kt => DvmImageToVideoRequestEvent.kt} (84%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageToVideo/{NIP90ImageToVideoResponseEvent.kt => DvmImageToVideoResponseEvent.kt} (81%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/malwareScanning/{NIP90MalwareScanRequestEvent.kt => DvmMalwareScanRequestEvent.kt} (84%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/malwareScanning/{NIP90MalwareScanResponseEvent.kt => DvmMalwareScanResponseEvent.kt} (81%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/opReturn/{NIP90OpReturnRequestEvent.kt => DvmOpReturnRequestEvent.kt} (85%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/opReturn/{NIP90OpReturnResponseEvent.kt => DvmOpReturnResponseEvent.kt} (82%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/peopleSearch/{NIP90PeopleSearchRequestEvent.kt => DvmPeopleSearchRequestEvent.kt} (87%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/peopleSearch/{NIP90PeopleSearchResponseEvent.kt => DvmPeopleSearchResponseEvent.kt} (83%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/status/{NIP90StatusEvent.kt => DvmStatusEvent.kt} (85%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/summarization/{NIP90SummarizationRequestEvent.kt => DvmSummarizationRequestEvent.kt} (85%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/summarization/{NIP90SummarizationResponseEvent.kt => DvmSummarizationResponseEvent.kt} (80%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textExtraction/{NIP90TextExtractionRequestEvent.kt => DvmTextExtractionRequestEvent.kt} (87%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textExtraction/{NIP90TextExtractionResponseEvent.kt => DvmTextExtractionResponseEvent.kt} (80%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textGeneration/{NIP90TextGenerationRequestEvent.kt => DvmTextGenerationRequestEvent.kt} (90%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textGeneration/{NIP90TextGenerationResponseEvent.kt => DvmTextGenerationResponseEvent.kt} (80%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textToSpeech/{NIP90TextToSpeechRequestEvent.kt => DvmTextToSpeechRequestEvent.kt} (87%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textToSpeech/{NIP90TextToSpeechResponseEvent.kt => DvmTextToSpeechResponseEvent.kt} (81%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/translation/{NIP90TranslationRequestEvent.kt => DvmTranslationRequestEvent.kt} (85%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/translation/{NIP90TranslationResponseEvent.kt => DvmTranslationResponseEvent.kt} (81%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/userDiscoveryRequest/{NIP90UserDiscoveryRequestEvent.kt => DvmUserDiscoveryRequestEvent.kt} (83%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/userDiscoveryResponse/{NIP90UserDiscoveryResponseEvent.kt => DvmUserDiscoveryResponseEvent.kt} (82%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoConversion/{NIP90VideoConversionRequestEvent.kt => DvmVideoConversionRequestEvent.kt} (86%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoConversion/{NIP90VideoConversionResponseEvent.kt => DvmVideoConversionResponseEvent.kt} (80%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoTranslation/{NIP90VideoTranslationRequestEvent.kt => DvmVideoTranslationRequestEvent.kt} (87%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoTranslation/{NIP90VideoTranslationResponseEvent.kt => DvmVideoTranslationResponseEvent.kt} (80%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/{FileHeaderEvent.kt => FileMetadataEvent.kt} (93%) rename quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/{LnZapPaymentRequestEventTest.kt => NwcRequestEventTest.kt} (93%) rename quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/{RelayFeedsListEventTest.kt => FavoriteRelayListEventTest.kt} (87%) create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/NutzapRedemptionTest.kt diff --git a/.claude/skills/auth-signers/SKILL.md b/.claude/skills/auth-signers/SKILL.md index 8b11a52d1b..0d32eedf8c 100644 --- a/.claude/skills/auth-signers/SKILL.md +++ b/.claude/skills/auth-signers/SKILL.md @@ -29,7 +29,7 @@ abstract class NostrSigner(val pubKey: HexKey) { abstract fun nip04Decrypt(ciphertext: String, fromPubKey: HexKey, onReady: (String) -> Unit) abstract fun nip44Encrypt(...) abstract fun nip44Decrypt(...) - abstract fun decryptZapEvent(event: LnZapRequestEvent, onReady: (LnZapRequestEvent) -> Unit) + abstract fun decryptZapEvent(event: ZapRequestEvent, onReady: (ZapRequestEvent) -> Unit) } ``` diff --git a/.claude/skills/nip85-trusted-assertions/SKILL.md b/.claude/skills/nip85-trusted-assertions/SKILL.md index b0b17f22ea..d75415e4df 100644 --- a/.claude/skills/nip85-trusted-assertions/SKILL.md +++ b/.claude/skills/nip85-trusted-assertions/SKILL.md @@ -26,16 +26,16 @@ event signatures; the 10040→assertion link is **consumer-side convention** (se | Kind | Class | Kind class | d-tag = the subject | Content | |---|---|---|---|---| | 10040 | `list/TrustProviderListEvent` | replaceable | *(none — always `""`)* | NIP-44 private provider entries (optional) | -| 30382 | `users/ContactCardEvent` | addressable | **target user's pubkey** (hex) | NIP-44 private tags (petname/summary/emoji) | +| 30382 | `users/UserAssertionEvent` | addressable | **target user's pubkey** (hex) | NIP-44 private tags (petname/summary/emoji) | | 30383 | `events/EventAssertionEvent` | addressable | **target event id** (hex) | `""` | | 30384 | `addressables/AddressableAssertionEvent` | addressable | **target coordinate** `kind:pubkey:dtag` | `""` | | 30385 | `externalIds/ExternalIdAssertionEvent` | addressable | **external identifier** (e.g. `isbn:978-0-13-468599-1`) | `""` | -Addresses: `ContactCardEvent.createAddress(owner, target)` → `Address(30382, owner, target)` +Addresses: `UserAssertionEvent.createAddress(owner, target)` → `Address(30382, owner, target)` (owner = signer, target = subject). `TrustProviderListEvent.createAddress(pubKey)` uses `FIXED_D_TAG = ""`. `AssertionEventTest.eventKindsAreCorrect` pins all five numbers. -`ContactCardEvent` is also a `SearchableEvent` — it indexes only the **public** petname/summary +`UserAssertionEvent` is also a `SearchableEvent` — it indexes only the **public** petname/summary tags plus topics; the encrypted card content is intentionally never indexed. ## The 10040 provider entry (`ServiceProviderTag` / `ServiceType`) @@ -91,7 +91,7 @@ The same kind serves two roles, distinguished **by author**: `followers`, `hops`, …); this is what 10040 discovery points at. 2. **The account's own contact cards (nicknames, NIP-81-style)** — signed by the account, one per target user. The petname, summary, and their NIP-30 emoji mappings **always live in - the NIP-44 encrypted content, never in public tags** (`ContactCardEvent.build`/ + the NIP-44 encrypted content, never in public tags** (`UserAssertionEvent.build`/ `updatePetNameAndSummary` strip stray public copies; asserted by `ContactCardPetNameTest`). `commons/.../ContactCardsState.kt` keys everything on `author == account` and ignores provider cards. @@ -104,7 +104,7 @@ value — a bad tag is *dropped*, never an error) + `assemble(value)` → `[name validation (rank isn't clamped, hours aren't checked against 0–23, counts may be negative) — consumers must defend. -**On 30382** (`users/tags/`, accessors on `ContactCardEvent` and as `TagArray` extensions in +**On 30382** (`users/tags/`, accessors on `UserAssertionEvent` and as `TagArray` extensions in `users/TagArrayExt.kt` so they also work on decrypted private arrays): | Tag name | Accessor | Type | Semantics | @@ -138,7 +138,7 @@ val providers: List<ServiceProviderTag> = updated.serviceProviders() // val private = updated.privateTags(signer)?.serviceProviders() // private side // Provider-style contact card (public metrics) — the GrapeRankPublisher pattern: -val card = ContactCardEvent.create( +val card = UserAssertionEvent.create( targetUser = subjectPubkey, signer = providerSigner, publicInitializer = { diff --git a/.claude/skills/nostr-expert/references/event-hierarchy.md b/.claude/skills/nostr-expert/references/event-hierarchy.md index 1ca0c0fa1c..cf6f6dfd12 100644 --- a/.claude/skills/nostr-expert/references/event-hierarchy.md +++ b/.claude/skills/nostr-expert/references/event-hierarchy.md @@ -79,28 +79,28 @@ companion object { ### Zap Request/Receipt (kinds 9734, 9735) ```kotlin -class LnZapRequestEvent(...) : Event(...) +class ZapRequestEvent(...) : Event(...) // Created by client, sent to Lightning Address -class LnZapEvent(...) : Event(...) +class ZapReceiptEvent(...) : Event(...) // Receipt from LSP, contains bolt11 + embedded zap request - val zapRequest: LnZapRequestEvent? by lazy { containedPost() } + val zapRequest: ZapRequestEvent? by lazy { containedPost() } val amount: BigDecimal? by lazy { /* parse from bolt11 */ } ``` ### Long-Form Content (kind 30023) ```kotlin -class LongTextNoteEvent(...) : BaseAddressableEvent(...) +class LongFormContentEvent(...) : BaseAddressableEvent(...) // Blog posts, articles // Addressable via kind:pubkey:d-tag ``` ### Lists (kinds 10000-30004) ```kotlin -sealed class PeopleListEvent : BaseAddressableEvent { - object MuteList : PeopleListEvent(10000) - object PinList : PeopleListEvent(10001) - object BookmarkList : PeopleListEvent(10003) +sealed class FollowSetEvent : BaseAddressableEvent { + object MuteList : FollowSetEvent(10000) + object PinList : FollowSetEvent(10001) + object BookmarkList : FollowSetEvent(10003) // ... 18 list types total } ``` @@ -279,7 +279,7 @@ val metadata2 = MetadataEvent.createNew(name = "Alice Updated", picture = "url2" ### Event Deletion ```kotlin // Delete events -val deletion = DeletionEvent.create( +val deletion = DeletionRequestEvent.create( deleteEvents = listOf(eventId1, eventId2), reason = "Spam", signer = signer diff --git a/.claude/skills/nostr-expert/references/nip-catalog.md b/.claude/skills/nostr-expert/references/nip-catalog.md index 78ed47ee11..996addba10 100644 --- a/.claude/skills/nostr-expert/references/nip-catalog.md +++ b/.claude/skills/nostr-expert/references/nip-catalog.md @@ -15,7 +15,7 @@ under `experimental/`**. The categorized list below may lag behind — | 04 | `nip04Dm/` | EncryptedDmEvent.kt | Legacy encrypted DMs (deprecated for NIP-17) | | 05 | `nip05DnsIdentifiers/` | UserHexResolver.kt, Nip05Client.kt | Internet identifiers; `resolveUserHexOrNull` resolves hex/npub/nprofile/`name@domain` → pubkey (see references/nip05-identifiers.md) | | 06 | `nip06KeyDerivation/` | Mnemonic-related | BIP-39 key derivation | -| 09 | `nip09Deletions/` | DeletionEvent.kt | Event deletion requests (kind 5) | +| 09 | `nip09Deletions/` | DeletionRequestEvent.kt | Event deletion requests (kind 5) | | 11 | `nip11RelayInfo/` | RelayInformation.kt | Relay metadata | | 13 | `nip13Pow/` | ProofOfWork.kt | Proof of work | | 14 | `nip14Subject/` | Subject tags | Subject tags for text notes | @@ -33,7 +33,7 @@ ent for NIP-04) | | 10 | `nip10Notes/` | TextNoteEvent.kt | Text notes with threading (kind 1) | | 18 | `nip18Reposts/` | RepostEvent.kt, GenericRepostEvent.kt | Reposts (kind 6, 16) | | 22 | `nip22Comments/` | CommentEvent.kt | Comments (kind 1111) | -| 23 | `nip23LongContent/` | LongTextNoteEvent.kt | Long-form content (kind 30023) | +| 23 | `nip23LongContent/` | LongFormContentEvent.kt | Long-form content (kind 30023) | | 25 | `nip25Reactions/` | ReactionEvent.kt | Reactions (kind 7) | | 31 | `nip31Alts/` | Alt tags | Alt description tags | | 36 | `nip36SensitiveContent/` | Content warnings | Content warning tags | @@ -62,7 +62,7 @@ ent for NIP-04) | | 46 | `nip46RemoteSigner/` | NostrConnectEvent.kt | Remote signer protocol (bunker) | | 47 | `nip47WalletConnect/` | Nostr Wallet Connect | Wallet connection protocol | | 56 | `nip56Reports/` | ReportEvent.kt | Reports (kind 1984) | -| 57 | `nip57Zaps/` | LnZapEvent.kt, LnZapRequestEvent.kt | Lightning zaps (kinds 9734, 9735) | +| 57 | `nip57Zaps/` | ZapReceiptEvent.kt, ZapRequestEvent.kt | Lightning zaps (kinds 9734, 9735) | | 58 | `nip58Badges/` | Badge events | Badge definitions & awards (kinds 30009, 8) | | 59 | `nip59Giftwrap/` | GiftWrapEvent.kt | Gift-wrapped events for privacy | | 75 | `nip75ZapGoals/` | ZapGoalEvent.kt | Zap goals (kind 9041) | @@ -76,7 +76,7 @@ ent for NIP-04) | | 35 | `nip35Torrents/` | Torrent events | Torrent tracking | | 52 | `nip52Calendar/` | Calendar events | Calendar time-based/date-based (kinds 31922-31925) | | 53 | `nip53LiveActivities/` | LiveActivitiesEvent.kt | Live events/streaming (kind 30311) | -| 54 | `nip54Wiki/` | WikiNoteEvent.kt | Wiki pages (kind 30818) | +| 54 | `nip54Wiki/` | WikiArticleEvent.kt | Wiki pages (kind 30818) | | 68 | `nip68Picture/` | Picture metadata | Picture metadata | | 71 | `nip71Video/` | 7 video event types | Video events (kinds 34235, 35235, 1234, 1235) | | 72 | `nip72ModCommunities/` | Community events | Moderated communities (kinds 34550, 34551, 9041) | @@ -84,7 +84,7 @@ ent for NIP-04) | | 89 | `nip89AppHandlers/` | AppDefinitionEvent.kt | App recommendations (kinds 31990, 31989) | | 90 | `nip90Dvms/` | DVM job events | Data Vending Machines (DVMs) (kinds 5000-7000) | | 92 | `nip92IMeta/` | IMeta tags | Image metadata tags | -| 94 | `nip94FileMetadata/` | FileHeaderEvent.kt, FileStorageEvent.kt | File metadata (kind 1063) | +| 94 | `nip94FileMetadata/` | FileMetadataEvent.kt, FileStorageEvent.kt | File metadata (kind 1063) | | 96 | `nip96FileStorage/` | HTTP file storage | HTTP-based file storage | | 99 | `nip99Classifieds/` | ClassifiedsEvent.kt | Classifieds/marketplace (kind 30402) | | A0 | `nipA0VoiceMessages/` | Voice messages | Voice message events | @@ -93,7 +93,7 @@ ent for NIP-04) | ### Web/Storage/Other | NIP | Directory | Key Files | Description | |-----|-----------|-----------|-------------| -| 38 | `nip38UserStatus/` | StatusEvent.kt | User status (kind 30315) | +| 38 | `nip38UserStatus/` | UserStatusEvent.kt | User status (kind 30315) | | 60 | `nip60Payment/` | Wallet events | Wallet info (kind 13194) | | 61 | `nip61PaymentRequest/` | Nut zaps | Cashu payment requests | | 64 | `nip64Chess/` | Chess moves | Chess move events | diff --git a/.claude/skills/quartz-integration/SKILL.md b/.claude/skills/quartz-integration/SKILL.md index 55dc9ad12f..8bd65f92e1 100644 --- a/.claude/skills/quartz-integration/SKILL.md +++ b/.claude/skills/quartz-integration/SKILL.md @@ -650,15 +650,15 @@ val custom = ReactionEvent.build("🤙", targetEvent) ### NIP-57 — Zap request (kind 9734) ```kotlin -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent -val template = LnZapRequestEvent.build( +val template = ZapRequestEvent.build( message = "Great post!", relays = listOf("wss://relay.damus.io"), target = targetEvent, - zapType = LnZapRequestEvent.ZapType.PUBLIC + zapType = ZapRequestEvent.ZapType.PUBLIC ) -val zapRequest: LnZapRequestEvent = signer.sign(template) +val zapRequest: ZapRequestEvent = signer.sign(template) ``` ### NIP-59 — Gift wrap / sealed DM (kind 1059 + 14) @@ -678,9 +678,9 @@ val (dmEvent, giftWrap) = NIP17Factory.create( ### NIP-23 — Long-form article (kind 30023) ```kotlin -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent -val template = LongTextNoteEvent.build( +val template = LongFormContentEvent.build( body = markdownContent, title = "My Article", image = "https://example.com/cover.jpg", @@ -882,16 +882,16 @@ use `Nip11RelayInformation.fromJson(json)`. | 0 | User metadata | 01 | `MetadataEvent` | | 1 | Text note | 10 | `TextNoteEvent` | | 3 | Follow list | 02 | `ContactListEvent` | -| 4 | Legacy DM | 04 | `PrivateDmEvent` | -| 5 | Deletion | 09 | `DeletionEvent` | +| 4 | Legacy DM | 04 | `EncryptedDmEvent` | +| 5 | Deletion | 09 | `DeletionRequestEvent` | | 6 | Repost | 18 | `RepostEvent` | | 7 | Reaction | 25 | `ReactionEvent` | | 14 | Chat message (sealed) | 17 | `NIP17GroupMessage` | | 1059 | Gift wrap | 59 | `GiftWrapEvent` | -| 9734 | Zap request | 57 | `LnZapRequestEvent` | -| 9735 | Zap receipt | 57 | `LnZapEvent` | +| 9734 | Zap request | 57 | `ZapRequestEvent` | +| 9735 | Zap receipt | 57 | `ZapReceiptEvent` | | 10002 | Relay list | 65 | `AdvertisedRelayListEvent` | -| 30023 | Long-form content | 23 | `LongTextNoteEvent` | +| 30023 | Long-form content | 23 | `LongFormContentEvent` | ## Related Skills diff --git a/.claude/skills/searchable-events/references/searchable-kinds.md b/.claude/skills/searchable-events/references/searchable-kinds.md index b2e3348127..92688128ec 100644 --- a/.claude/skills/searchable-events/references/searchable-kinds.md +++ b/.claude/skills/searchable-events/references/searchable-kinds.md @@ -26,7 +26,7 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`. | 42 | ChannelMessageEvent | nip28PublicChat/message | `content` | | 54 | PodcastEpisodeEvent | nipF4Podcasts/episode | `listOfNotNull(title(), description(), content)` NL | | 1010 | TextNoteModificationEvent | experimental/edits | `listOfNotNull(content, summary())` NL (content first) | -| 1063 | FileHeaderEvent | nip94FileMetadata | `listOfNotNull(summary(), content)` NL | +| 1063 | FileMetadataEvent | nip94FileMetadata | `listOfNotNull(summary(), content)` NL | | 1065 | FileStorageHeaderEvent | experimental/nip95/header | `listOfNotNull(summary())` NL | | 1068 | PollEvent | nip88Polls/poll | `buildString { append(content); options().forEach { append('\n').append(it.label) } }` | | 1111 | CommentEvent | nip22Comments | `(listOf(content) + tags.hashtags())` NL | @@ -48,20 +48,20 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`. | 2004 | TorrentCommentEvent | nip35Torrents | `content` | | 2473 | BirdDetectionEvent | experimental/birdstar | `listOfNotNull(summary(), speciesName())` NL | | 3302 | ConcordChatEditEvent | concord/cord03Channels | `content` | -| 5050 | NIP90TextGenerationRequestEvent | nip90Dvms/textGeneration | `inputs().filter { it.type == "prompt" \|\| it.type == "text" }.joinToString(" ") { it.value }` (SP) | -| 5100 | NIP90ImageGenerationRequestEvent | nip90Dvms/imageGeneration | `listOfNotNull(prompt(), negativePrompt()).joinToString(" ")` (SP) | +| 5050 | DvmTextGenerationRequestEvent | nip90Dvms/textGeneration | `inputs().filter { it.type == "prompt" \|\| it.type == "text" }.joinToString(" ") { it.value }` (SP) | +| 5100 | DvmImageGenerationRequestEvent | nip90Dvms/imageGeneration | `listOfNotNull(prompt(), negativePrompt()).joinToString(" ")` (SP) | | 5129 | NappletSnapshotEvent | nip5dNapplets | `listOfNotNull(title(), description())` NL | -| 5250 | NIP90TextToSpeechRequestEvent | nip90Dvms/textToSpeech | `text() ?: ""` | -| 5302 | NIP90ContentSearchRequestEvent | nip90Dvms/contentSearch | `searchQuery() ?: ""` | -| 5303 | NIP90PeopleSearchRequestEvent | nip90Dvms/peopleSearch | `searchQuery() ?: ""` | +| 5250 | DvmTextToSpeechRequestEvent | nip90Dvms/textToSpeech | `text() ?: ""` | +| 5302 | DvmContentSearchRequestEvent | nip90Dvms/contentSearch | `searchQuery() ?: ""` | +| 5303 | DvmPeopleSearchRequestEvent | nip90Dvms/peopleSearch | `searchQuery() ?: ""` | | 6969 | ZapPollEvent | experimental/zapPolls | `buildString { append(content); pollOptionsArray().forEach { append('\n').append(it.descriptor) } }` | | 7516 | GeocacheFoundLogEvent | nipCCGeocaching/foundLog | `content` | | 8333 | OnchainZapEvent | nipBCOnchainZaps/zap | `content` | -| 9002 | EditMetadataEvent | nip29RelayGroups/moderation | `(listOfNotNull(name(), about()) + hashtags())` NL | -| 9041 | GoalEvent | nip75ZapGoals | `listOfNotNull(summary(), content)` NL | +| 9002 | GroupEditMetadataEvent | nip29RelayGroups/moderation | `(listOfNotNull(name(), about()) + hashtags())` NL | +| 9041 | ZapGoalEvent | nip75ZapGoals | `listOfNotNull(summary(), content)` NL | | 9321 | NutzapEvent | nip61Nutzaps/nutzap | `content` | -| 9734 | LnZapRequestEvent | nip57Zaps | `content` | -| 9735 | LnZapEvent | nip57Zaps | `zapRequest?.content.orEmpty()` — indexes the **embedded 9734's** content | +| 9734 | ZapRequestEvent | nip57Zaps | `content` | +| 9735 | ZapReceiptEvent | nip57Zaps | `zapRequest?.content.orEmpty()` — indexes the **embedded 9734's** content | | 9736 | Bolt12ZapEvent | nipB1Bolt12Zaps/zap | `content` | | 9737 | Bolt12ZapIntentEvent | nipB1Bolt12Zaps/intent | `content` | | 9802 | HighlightEvent | nip84Highlights | `listOfNotNull(comment(), context(), content)` NL | @@ -74,10 +74,10 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`. | 12473 | BirdexEvent | experimental/birdstar | `(listOfNotNull(summary()) + speciesNames())` NL | | 15128 | RootSiteEvent | nip5aStaticWebsites | `listOfNotNull(title(), description())` NL | | 15129 | RootNappletEvent | nip5dNapplets | `listOfNotNull(title(), description())` NL | -| 30000 | PeopleListEvent | nip51Lists/peopleList | `listOfNotNull(titleOrName(), description())` NL | +| 30000 | FollowSetEvent | nip51Lists/peopleList | `listOfNotNull(titleOrName(), description())` NL | | 30001 | OldBookmarkListEvent | nip51Lists/bookmarkList | `listOfNotNull(title())` NL | | 30002 | RelaySetEvent | nip51Lists/relaySets | `listOfNotNull(title(), description())` NL | -| 30003 | LabeledBookmarkListEvent | nip51Lists/labeledBookmarkList | `listOfNotNull(titleOrName(), description())` NL | +| 30003 | BookmarkSetEvent | nip51Lists/labeledBookmarkList | `listOfNotNull(titleOrName(), description())` NL | | 30004 | ArticleCurationSetEvent | nip51Lists/articleCurationSet | `listOfNotNull(title(), description())` NL | | 30005 | VideoCurationSetEvent | nip51Lists/videoCurationSet | `listOfNotNull(title(), description())` NL | | 30006 | PictureCurationSetEvent | nip51Lists/pictureCurationSet | `listOfNotNull(title(), description())` NL | @@ -87,7 +87,7 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`. | 30018 | ProductEvent | nip15Marketplace/product | `productData()?.let { (listOfNotNull(it.name, it.description) + categories()).joinToString("\n") } ?: ""` | | 30019 | MarketplaceEvent | nip15Marketplace/marketplace | `marketplaceData()?.let { listOfNotNull(it.name, it.about).joinToString("\n") } ?: ""` | | 30020 | AuctionEvent | nip15Marketplace/auction | `auctionData()?.let { (listOfNotNull(it.name, it.description) + tags.hashtags()).joinToString("\n") } ?: ""` | -| 30023 | LongTextNoteEvent | nip23LongContent | `listOfNotNull(title(), summary(), content)` NL | +| 30023 | LongFormContentEvent | nip23LongContent | `listOfNotNull(title(), summary(), content)` NL | | 30030 | EmojiPackEvent | nip30CustomEmoji/pack | `listOfNotNull(titleOrName(), description(), content)` NL | | 30054 | Podcasting20EpisodeEvent | nipXXPodcasting20/episode | `(listOfNotNull(title(), description(), content) + topics())` NL | | 30055 | Podcasting20TrailerEvent | nipXXPodcasting20/trailer | `listOfNotNull(title(), content)` NL | @@ -101,8 +101,8 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`. | 30311 | LiveActivitiesEvent | nip53LiveActivities/streaming | `listOfNotNull(title(), summary(), content)` NL | | 30312 | MeetingSpaceEvent | nip53LiveActivities/meetingSpaces | `listOfNotNull(room(), summary(), content)` NL | | 30313 | MeetingRoomEvent | nip53LiveActivities/meetingSpaces | `listOfNotNull(title(), summary())` NL | -| 30315 | StatusEvent | nip38UserStatus | `content` | -| 30382 | ContactCardEvent | nip85TrustedAssertions/users | `(listOfNotNull(petName(), summary()) + topics())` NL — public tags only, never the NIP-44 content | +| 30315 | UserStatusEvent | nip38UserStatus | `content` | +| 30382 | UserAssertionEvent | nip85TrustedAssertions/users | `(listOfNotNull(petName(), summary()) + topics())` NL — public tags only, never the NIP-44 content | | 30392 | UserTrustedListEvent | experimental/trustedLists/users | inherited `TrustedListEvent`: `title() ?: ""` — the label only; `metric`/`d` are machine ids and `content` is a JSON echo of the membership | | 30393 | EventTrustedListEvent | experimental/trustedLists/events | inherited `TrustedListEvent`: `title() ?: ""` | | 30394 | AddressableTrustedListEvent | experimental/trustedLists/addressables | inherited `TrustedListEvent`: `title() ?: ""` | @@ -111,7 +111,7 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`. | 30617 | GitRepositoryEvent | nip34Git/repository | `listOfNotNull(name(), description(), content)` NL | | 30620 | WorkflowDefEvent | buzz/workflow | `listOfNotNull(name(), content)` NL | | 30817 | NipTextEvent | experimental/nipsOnNostr | `listOfNotNull(title(), content)` NL | -| 30818 | WikiNoteEvent | nip54Wiki | `listOfNotNull(title(), summary(), content)` NL | +| 30818 | WikiArticleEvent | nip54Wiki | `listOfNotNull(title(), summary(), content)` NL | | 31337 | AudioTrackEvent | experimental/audio/track | `listOfNotNull(subject())` NL | | 31871 | AttestationEvent | experimental/attestations/attestation | `content` | | 31872 | AttestationRequestEvent | experimental/attestations/request | `content` | @@ -119,15 +119,15 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`. | 31890 | FeedDefinitionEvent | feedDefinition | `title().orEmpty()` | | 31922 | CalendarDateSlotEvent | nip52Calendar/appt/day | `listOfNotNull(title(), summary(), content)` NL | | 31923 | CalendarTimeSlotEvent | nip52Calendar/appt/time | `listOfNotNull(title(), summary(), content)` NL | -| 31924 | CalendarEvent | nip52Calendar/calendar | `listOfNotNull(title(), content)` NL | +| 31924 | CalendarCollectionEvent | nip52Calendar/calendar | `listOfNotNull(title(), content)` NL | | 31925 | CalendarRSVPEvent | nip52Calendar/rsvp | `content` | | 31990 | AppDefinitionEvent | nip89AppHandlers/definition | `appMetaData()?.let { listOfNotNull(it.name, it.username, it.displayName, it.about, it.nip05, it.lud06, it.lud16, it.website, it.picture, it.banner, it.image).joinToString(" ") } ?: ""` (SP) | | 32267 | SoftwareApplicationEvent | experimental/nip82SoftwareApps/application | `listOfNotNull(name(), summary(), content)` NL | | 33401 | ExerciseTemplateEvent | experimental/fitness/workout | `listOfNotNull(title(), content)` NL | | 33863 | FundraiserEvent | experimental/agora | `listOfNotNull(title(), content)` NL | | 34139 | MusicPlaylistEvent | experimental/music/playlist | `listOfNotNull(title(), description(), content)` NL | -| 34235 | VideoHorizontalEvent | nip71Video | inherited `AddressableVideoEvent`: `listOfNotNull(title(), content)` NL | -| 34236 | VideoVerticalEvent | nip71Video | inherited `AddressableVideoEvent`: `listOfNotNull(title(), content)` NL | +| 34235 | AddressableNormalVideoEvent | nip71Video | inherited `AddressableVideoEvent`: `listOfNotNull(title(), content)` NL | +| 34236 | AddressableShortVideoEvent | nip71Video | inherited `AddressableVideoEvent`: `listOfNotNull(title(), content)` NL | | 34550 | CommunityDefinitionEvent | nip72ModCommunities/definition | `listOfNotNull(name(), description(), rules(), content)` NL | | 35128 | NamedSiteEvent | nip5aStaticWebsites | `listOfNotNull(title(), description())` NL | | 35129 | NamedNappletEvent | nip5dNapplets | `listOfNotNull(title(), description())` NL | @@ -138,7 +138,7 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`. | 38192 | Ps1SaveEvent | experimental/ps1saves | `listOfNotNull(summary(), saveTitle(), region(), filename())` NL | | 38383 | P2POrderEvent | nip69P2pOrderEvents | `(listOfNotNull(makerName(), currency()) + paymentMethods().orEmpty()).joinToString(" ")` (SP) | | 39000 | GroupMetadataEvent | nip29RelayGroups/metadata | `listOfNotNull(name(), about())` NL | -| 39089 | FollowListEvent | nip51Lists/followList | `listOfNotNull(title(), description())` NL | +| 39089 | StarterPackEvent | nip51Lists/followList | `listOfNotNull(title(), description())` NL | | 39092 | MediaStarterPackEvent | nip51Lists/mediaStarterPack | `listOfNotNull(title(), description())` NL | | 39307 | TextTrackEvent | nip71Video/textTrack | `content` (the WebVTT cue text) | | 39701 | WebBookmarkEvent | nipB0WebBookmarks | `listOfNotNull(title(), description())` NL | diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index a0acb8bad9..7f1b0da472 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -54,19 +54,19 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip19Bech32.toNpub -import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent +import com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListEvent import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent @@ -887,19 +887,19 @@ object LocalPreferences { val pendingAttestations = async { parseOrNull<Map<HexKey, String>>(pendingAttestationsStr) ?: mapOf() } val latestUserMetadata = async { parseEventOrNull<MetadataEvent>(latestUserMetadataStr) } val latestContactList = async { parseEventOrNull<ContactListEvent>(latestContactListStr) } - val latestDmRelayList = async { parseEventOrNull<ChatMessageRelayListEvent>(latestDmRelayListStr) } + val latestDmRelayList = async { parseEventOrNull<DmRelayListEvent>(latestDmRelayListStr) } val latestNip65RelayList = async { parseEventOrNull<AdvertisedRelayListEvent>(latestNip65RelayListStr) } val latestSearchRelayList = async { parseEventOrNull<SearchRelayListEvent>(latestSearchRelayListStr) } val latestIndexRelayList = async { parseEventOrNull<IndexerRelayListEvent>(latestIndexRelayListStr) } - val latestRelayFeedsList = async { parseEventOrNull<RelayFeedsListEvent>(latestRelayFeedsListStr) } + val latestRelayFeedsList = async { parseEventOrNull<FavoriteRelayListEvent>(latestRelayFeedsListStr) } val latestBlockedRelayList = async { parseEventOrNull<BlockedRelayListEvent>(latestBlockedRelayListStr) } val latestTrustedRelayList = async { parseEventOrNull<TrustedRelayListEvent>(latestTrustedRelayListStr) } val latestMuteList = async { parseEventOrNull<MuteListEvent>(latestMuteListStr) } val latestPrivateHomeRelayList = async { parseEventOrNull<PrivateOutboxRelayListEvent>(latestPrivateHomeRelayListStr) } val latestAppSpecificData = async { parseEventOrNull<AppSpecificDataEvent>(latestAppSpecificDataStr) } - val latestChannelList = async { parseEventOrNull<ChannelListEvent>(latestChannelListStr) } + val latestChannelList = async { parseEventOrNull<PublicChatListEvent>(latestChannelListStr) } val latestCommunityList = async { parseEventOrNull<CommunityListEvent>(latestCommunityListStr) } - val latestHashtagList = async { parseEventOrNull<HashtagListEvent>(latestHashtagListStr) } + val latestHashtagList = async { parseEventOrNull<InterestListEvent>(latestHashtagListStr) } val latestGeohashList = async { parseEventOrNull<GeohashListEvent>(latestGeohashListStr) } val latestEphemeralList = async { parseEventOrNull<EphemeralChatListEvent>(latestEphemeralListStr) } val latestRelayGroupList = async { parseEventOrNull<SimpleGroupListEvent>(latestRelayGroupListStr) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 406c21e081..438be42d4f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -238,8 +238,8 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds import com.vitorpamplona.quartz.nip01Core.tags.references.references import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip10Notes.content.findHashtags import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris @@ -281,10 +281,10 @@ import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportType -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.PrivateZapCache +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser @@ -320,10 +320,10 @@ import com.vitorpamplona.quartz.nip88Polls.poll.tags.PollType import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent import com.vitorpamplona.quartz.nip89AppHandlers.clientTag.NostrSignerWithClientTag import com.vitorpamplona.quartz.nip89AppHandlers.clientTag.withoutClientTag -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.DvmContentDiscoveryRequestEvent import com.vitorpamplona.quartz.nip92IMeta.IMetaTag import com.vitorpamplona.quartz.nip92IMeta.imetas -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent import com.vitorpamplona.quartz.nip94FileMetadata.blurhash import com.vitorpamplona.quartz.nip94FileMetadata.dimension import com.vitorpamplona.quartz.nip94FileMetadata.fileSize @@ -1283,7 +1283,7 @@ class Account( suspend fun updateZapAmounts( amountSet: List<Long>, - selectedZapType: LnZapEvent.ZapType, + selectedZapType: ZapReceiptEvent.ZapType, nip47Update: Nip47WalletConnect.Nip47URINorm?, ) { var changed = false @@ -1707,7 +1707,7 @@ class Account( if (myNotes.isNotEmpty()) { // chunks in 200 elements to avoid going over the 65KB limit for events. myNotes.chunked(200).forEach { chunkedList -> - val template = DeletionEvent.build(chunkedList.mapNotNull { it.event }) + val template = DeletionRequestEvent.build(chunkedList.mapNotNull { it.event }) val deletionEvent = signer.sign(template) val myRelayList = outboxRelays.flow.value.toMutableSet() chunkedList.forEach { @@ -1738,7 +1738,7 @@ class Account( val recipients = (targetEvent.taggedUserIds() + targetEvent.pubKey).distinct().minus(signer.pubKey) broadcastPrivately( - NIP17Factory().createDeletionNIP17(DeletionEvent.build(myRumors), recipients, signer), + NIP17Factory().createDeletionNIP17(DeletionRequestEvent.build(myRumors), recipients, signer), ) } @@ -1749,7 +1749,7 @@ class Account( if (!isWriteable()) return if (event.pubKey != signer.pubKey) return - val deletionEvent = signer.sign(DeletionEvent.build(listOf(event))) + val deletionEvent = signer.sign(DeletionRequestEvent.build(listOf(event))) client.publish(deletionEvent, outboxRelays.flow.value + additionalRelays) cache.justConsumeMyOwnEvent(deletionEvent) } @@ -2176,7 +2176,7 @@ class Account( suspend fun deleteWebBookmark(event: WebBookmarkEvent) { if (!isWriteable()) return - val template = DeletionEvent.build(listOf(event)) + val template = DeletionRequestEvent.build(listOf(event)) val signedEvent = signer.sign(template) cache.justConsumeMyOwnEvent(signedEvent) @@ -2212,7 +2212,7 @@ class Account( if (!isWriteable()) return if (event.pubKey != signer.pubKey) return - val template = DeletionEvent.build(listOf(event)) + val template = DeletionRequestEvent.build(listOf(event)) val signedEvent = signer.sign(template) cache.justConsumeMyOwnEvent(signedEvent) @@ -2597,7 +2597,7 @@ class Account( } } } else { - FileHeaderEvent.build(url, alt) { + FileMetadataEvent.build(url, alt) { hash(headerInfo.hash) fileSize(headerInfo.size) @@ -2711,7 +2711,7 @@ class Account( val extraRelays = draftNote.relays val deletedDraft = DraftWrapEvent.createDeletedEvent(draftTag, signer) - val deletionEvent = signer.sign(DeletionEvent.build(listOf(deletedDraft))) + val deletionEvent = signer.sign(DeletionRequestEvent.build(listOf(deletedDraft))) val relayList = (privateStorageRelayList.flow.value + localRelayList.flow.value + extraRelays).toSet() @@ -2937,7 +2937,7 @@ class Account( broadcast.forEach { client.publish(it, relayList) } } - override suspend fun sendNip04PrivateMessage(eventTemplate: EventTemplate<PrivateDmEvent>) { + override suspend fun sendNip04PrivateMessage(eventTemplate: EventTemplate<EncryptedDmEvent>) { if (!isWriteable()) return val newEvent = signer.sign(eventTemplate) @@ -3481,10 +3481,10 @@ class Account( suspend fun requestDVMContentDiscovery( dvmPublicKey: User, - onReady: (event: NIP90ContentDiscoveryRequestEvent, relays: Set<NormalizedRelayUrl>) -> Unit, + onReady: (event: DvmContentDiscoveryRequestEvent, relays: Set<NormalizedRelayUrl>) -> Unit, ) { val relays = nip65RelayList.inboxFlow.value.toSet() - val request = signer.sign<NIP90ContentDiscoveryRequestEvent>(NIP90ContentDiscoveryRequestEvent.build(dvmPublicKey.pubkeyHex, signer.pubKey, relays)) + val request = signer.sign<DvmContentDiscoveryRequestEvent>(DvmContentDiscoveryRequestEvent.build(dvmPublicKey.pubkeyHex, signer.pubKey, relays)) val relayList = dvmPublicKey.inboxRelays()?.toSet()?.ifEmpty { null } @@ -3503,8 +3503,8 @@ class Account( return if (isWriteable()) { when { - event is PrivateDmEvent -> privateDMDecryptionCache.cachedDM(event) - event is LnZapRequestEvent && event.isPrivateZap() -> privateZapsDecryptionCache.cachedPrivateZap(event)?.content + event is EncryptedDmEvent -> privateDMDecryptionCache.cachedDM(event) + event is ZapRequestEvent && event.isPrivateZap() -> privateZapsDecryptionCache.cachedPrivateZap(event)?.content event is DraftWrapEvent -> draftsDecryptionCache.preCachedDraft(event)?.content else -> event.content } @@ -3519,11 +3519,11 @@ class Account( suspend fun decryptContent(note: Note): String? { val event = note.event return when { - event is PrivateDmEvent && isWriteable() -> { + event is EncryptedDmEvent && isWriteable() -> { privateDMDecryptionCache.decryptDM(event) } - event is LnZapRequestEvent && isWriteable() -> { + event is ZapRequestEvent && isWriteable() -> { if (event.isPrivateZap()) { if (isWriteable()) { privateZapsDecryptionCache.decryptPrivateZap(event)?.content @@ -3550,7 +3550,7 @@ class Account( } } - suspend fun decryptZapOrNull(event: LnZapRequestEvent): LnZapPrivateEvent? = if (event.isPrivateZap() && isWriteable()) privateZapsDecryptionCache.decryptPrivateZap(event) else null + suspend fun decryptZapOrNull(event: ZapRequestEvent): LnZapPrivateEvent? = if (event.isPrivateZap() && isWriteable()) privateZapsDecryptionCache.decryptPrivateZap(event) else null fun isAllHidden(users: Set<HexKey>): Boolean = users.all { isHidden(it) } @@ -3602,7 +3602,7 @@ class Account( note.countReportAuthorsBy(followingKeySet()) < reportWarningThreshold } - fun isDecryptedContentHidden(noteEvent: PrivateDmEvent): Boolean = + fun isDecryptedContentHidden(noteEvent: EncryptedDmEvent): Boolean = if (hiddenUsers.flow.value.hiddenWordsCase .isNotEmpty() ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountRelayGroupActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountRelayGroupActions.kt index 33612b11ef..884213d17f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountRelayGroupActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountRelayGroupActions.kt @@ -58,15 +58,15 @@ import com.vitorpamplona.quartz.nip29RelayGroups.GroupId import com.vitorpamplona.quartz.nip29RelayGroups.hTag import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateGroupEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateInviteEvent import com.vitorpamplona.quartz.nip29RelayGroups.moderation.DeleteGroupEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.EditMetadataEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.PutUserEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.RemoveUserEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.UpdatePinListEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupCreateInviteEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupEditMetadataEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupPutUserEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupRemoveUserEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupUpdatePinListEvent import com.vitorpamplona.quartz.nip29RelayGroups.moderation.previous -import com.vitorpamplona.quartz.nip29RelayGroups.request.JoinRequestEvent -import com.vitorpamplona.quartz.nip29RelayGroups.request.LeaveRequestEvent +import com.vitorpamplona.quartz.nip29RelayGroups.request.GroupJoinRequestEvent +import com.vitorpamplona.quartz.nip29RelayGroups.request.GroupLeaveRequestEvent import com.vitorpamplona.quartz.nip29RelayGroups.tags.GroupIdTag import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent import com.vitorpamplona.quartz.utils.RandomInstance @@ -92,7 +92,7 @@ class AccountRelayGroupActions( channel: RelayGroupChannel, code: String? = null, ) { - val template = JoinRequestEvent.build(channel.groupId.id, inviteCode = code) + val template = GroupJoinRequestEvent.build(channel.groupId.id, inviteCode = code) account.broadcaster.signAndSendPrivatelyOrBroadcast(template) { channel.relays().toList() } account.follow(channel) } @@ -295,7 +295,7 @@ class AccountRelayGroupActions( /** Send a kind 9022 leave request to the host relay and drop it from our list. */ suspend fun leaveRelayGroup(channel: RelayGroupChannel) { - val template = LeaveRequestEvent.build(channel.groupId.id) + val template = GroupLeaveRequestEvent.build(channel.groupId.id) account.broadcaster.signAndSendPrivatelyOrBroadcast(template) { channel.relays().toList() } account.unfollow(channel) } @@ -351,7 +351,7 @@ class AccountRelayGroupActions( ) { listOf(relay) } val edit = - EditMetadataEvent.build( + GroupEditMetadataEvent.build( groupId, name = name, about = about, @@ -405,7 +405,7 @@ class AccountRelayGroupActions( channel: RelayGroupChannel, code: String, ) { - val template = CreateInviteEvent.build(channel.groupId.id, code) + val template = GroupCreateInviteEvent.build(channel.groupId.id, code) account.broadcaster.signAndSendPrivatelyOrBroadcast(template) { channel.relays().toList() } } @@ -418,7 +418,7 @@ class AccountRelayGroupActions( channel: RelayGroupChannel, pinnedEventIds: List<HexKey>, ) { - val template = UpdatePinListEvent.build(channel.groupId.id, pinnedEventIds) + val template = GroupUpdatePinListEvent.build(channel.groupId.id, pinnedEventIds) account.broadcaster.signAndSendPrivatelyOrBroadcast(template) { channel.relays().toList() } } @@ -445,7 +445,7 @@ class AccountRelayGroupActions( channel: RelayGroupChannel, pubkey: HexKey, ) { - val template = RemoveUserEvent.build(channel.groupId.id, listOf(pubkey)) + val template = GroupRemoveUserEvent.build(channel.groupId.id, listOf(pubkey)) account.broadcaster.signAndSendPrivatelyOrBroadcast(template) { channel.relays().toList() } } @@ -470,7 +470,7 @@ class AccountRelayGroupActions( } else { null } - val template = PutUserEvent.build(channel.groupId.id, listOf(pubkey to roles), buzzRole = buzzRole) + val template = GroupPutUserEvent.build(channel.groupId.id, listOf(pubkey to roles), buzzRole = buzzRole) account.broadcaster.signAndSendPrivatelyOrBroadcast(template) { channel.relays().toList() } } @@ -525,7 +525,7 @@ class AccountRelayGroupActions( // edit when we send that tag. A plain NIP-29 relay ignores it and honours the status flag. val isBuzz = BuzzRelayDialect.isBuzz(channel.groupId.relayUrl) val template = - EditMetadataEvent.build( + GroupEditMetadataEvent.build( channel.groupId.id, name = name, about = about, @@ -549,7 +549,7 @@ class AccountRelayGroupActions( channel: RelayGroupChannel, archived: Boolean, ) { - val template = EditMetadataEvent.build(channel.groupId.id, archived = archived) + val template = GroupEditMetadataEvent.build(channel.groupId.id, archived = archived) account.broadcaster.signAndSendPrivatelyOrBroadcast(template) { channel.relays().toList() } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index e2613fbe36..b70197390c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -59,8 +59,8 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent -import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent +import com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent @@ -68,16 +68,16 @@ import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip60Cashu.wallet.CashuWalletEvent import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent @@ -203,20 +203,20 @@ class AccountSettings( val showMessagesInNotifications: MutableStateFlow<Boolean> = MutableStateFlow(true), var backupUserMetadata: MetadataEvent? = null, var backupContactList: ContactListEvent? = null, - var backupDMRelayList: ChatMessageRelayListEvent? = null, + var backupDMRelayList: DmRelayListEvent? = null, var backupKeyPackageRelayList: KeyPackageRelayListEvent? = null, var backupNIP65RelayList: AdvertisedRelayListEvent? = null, var backupSearchRelayList: SearchRelayListEvent? = null, var backupIndexRelayList: IndexerRelayListEvent? = null, - var backupRelayFeedsList: RelayFeedsListEvent? = null, + var backupRelayFeedsList: FavoriteRelayListEvent? = null, var backupBlockedRelayList: BlockedRelayListEvent? = null, var backupTrustedRelayList: TrustedRelayListEvent? = null, var backupMuteList: MuteListEvent? = null, var backupPrivateHomeRelayList: PrivateOutboxRelayListEvent? = null, var backupAppSpecificData: AppSpecificDataEvent? = null, - var backupChannelList: ChannelListEvent? = null, + var backupChannelList: PublicChatListEvent? = null, var backupCommunityList: CommunityListEvent? = null, - var backupHashtagList: HashtagListEvent? = null, + var backupHashtagList: InterestListEvent? = null, var backupFavoriteAlgoFeedsList: FavoriteAlgoFeedsListEvent? = null, var backupGeohashList: GeohashListEvent? = null, var backupEphemeralChatList: EphemeralChatListEvent? = null, @@ -375,7 +375,7 @@ class AccountSettings( // Zaps and Reactions // --- - fun changeDefaultZapType(zapType: LnZapEvent.ZapType): Boolean { + fun changeDefaultZapType(zapType: ZapReceiptEvent.ZapType): Boolean { if (syncedSettings.zaps.defaultZapType.value != zapType) { syncedSettings.zaps.defaultZapType.tryEmit(zapType) saveAccountSettings() @@ -1075,7 +1075,7 @@ class AccountSettings( when (incoming) { is MetadataEvent -> updateUserMetadata(incoming) is ContactListEvent -> updateContactListTo(incoming) - is ChatMessageRelayListEvent -> updateDMRelayList(incoming) + is DmRelayListEvent -> updateDMRelayList(incoming) is KeyPackageRelayListEvent -> updateKeyPackageRelayList(incoming) is AdvertisedRelayListEvent -> updateNIP65RelayList(incoming) is CashuWalletEvent -> updateCashuWallet(incoming) @@ -1084,13 +1084,13 @@ class AccountSettings( is Bolt12OfferListEvent -> updateBolt12Offers(incoming) is SearchRelayListEvent -> updateSearchRelayList(incoming) is IndexerRelayListEvent -> updateIndexRelayList(incoming) - is RelayFeedsListEvent -> updateRelayFeedList(incoming) + is FavoriteRelayListEvent -> updateRelayFeedList(incoming) is BlockedRelayListEvent -> updateBlockedRelayList(incoming) is TrustedRelayListEvent -> updateTrustedRelayList(incoming) is PrivateOutboxRelayListEvent -> updatePrivateHomeRelayList(incoming) - is ChannelListEvent -> updateChannelListTo(incoming) + is PublicChatListEvent -> updateChannelListTo(incoming) is GeohashListEvent -> updateGeohashListTo(incoming) - is HashtagListEvent -> updateHashtagListTo(incoming) + is InterestListEvent -> updateHashtagListTo(incoming) is FavoriteAlgoFeedsListEvent -> updateFavoriteAlgoFeedsListTo(incoming) is CommunityListEvent -> updateCommunityListTo(incoming) is EphemeralChatListEvent -> updateEphemeralChatListTo(incoming) @@ -1140,7 +1140,7 @@ class AccountSettings( } } - fun updateDMRelayList(newDMRelayList: ChatMessageRelayListEvent?) { + fun updateDMRelayList(newDMRelayList: DmRelayListEvent?) { if (newDMRelayList == null) return if (backupGuard.accept(backupDMRelayList, newDMRelayList, isEmpty = newDMRelayList.tags.isEmpty()) { updateDMRelayList(newDMRelayList) }) { @@ -1284,7 +1284,7 @@ class AccountSettings( } } - fun updateRelayFeedList(newRelayFeedList: RelayFeedsListEvent?) { + fun updateRelayFeedList(newRelayFeedList: FavoriteRelayListEvent?) { if (newRelayFeedList == null) return if (backupGuard.accept(backupRelayFeedsList, newRelayFeedList, isEmpty = newRelayFeedList.tags.isEmpty()) { updateRelayFeedList(newRelayFeedList) }) { @@ -1322,7 +1322,7 @@ class AccountSettings( override fun channelList() = backupChannelList - override fun updateChannelListTo(newChannelList: ChannelListEvent?) { + override fun updateChannelListTo(newChannelList: PublicChatListEvent?) { if (newChannelList == null) return if (backupGuard.accept(backupChannelList, newChannelList, isEmpty = newChannelList.tags.isEmpty()) { updateChannelListTo(newChannelList) }) { @@ -1340,7 +1340,7 @@ class AccountSettings( } } - fun updateHashtagListTo(newHashtagList: HashtagListEvent?) { + fun updateHashtagListTo(newHashtagList: InterestListEvent?) { if (newHashtagList == null) return if (backupGuard.accept(backupHashtagList, newHashtagList, isEmpty = newHashtagList.tags.isEmpty()) { updateHashtagListTo(newHashtagList) }) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt index 3d1047d25a..3acb2ed75a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.amethyst.commons.service.pow.PoWCategory import com.vitorpamplona.amethyst.commons.service.pow.PoWPolicy import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow @@ -265,7 +265,7 @@ class AccountVideoPlayerPreferences( @Stable class AccountZapPreferences( var zapAmountChoices: MutableStateFlow<ImmutableList<Long>>, - val defaultZapType: MutableStateFlow<LnZapEvent.ZapType>, + val defaultZapType: MutableStateFlow<ZapReceiptEvent.ZapType>, ) /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt index b167b7ba62..de721aefbe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt @@ -25,7 +25,7 @@ import androidx.core.os.ConfigurationCompat import com.vitorpamplona.amethyst.commons.model.navigation.BottomBarEntry import com.vitorpamplona.amethyst.commons.service.pow.PoWCategory import com.vitorpamplona.amethyst.ui.navigation.bottombars.DefaultBottomBarEntries -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import kotlinx.serialization.Serializable import java.util.Locale @@ -207,7 +207,7 @@ class AccountZapPreferencesInternal( // and so the on-chain-eligible subset round-trips back for them; on load it // is unioned into [zapAmountChoices]. See AccountSyncedSettings. var onchainZapAmountChoices: List<Long> = DefaultOnchainZapAmounts, - val defaultZapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PUBLIC, + val defaultZapType: ZapReceiptEvent.ZapType = ZapReceiptEvent.ZapType.PUBLIC, ) @Serializable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountZapActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountZapActions.kt index 4bcfcaef46..cf994956f6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountZapActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountZapActions.kt @@ -48,8 +48,8 @@ import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayMethod import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaySuccessResponse import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.builder.Bolt12ZapBuilder import com.vitorpamplona.quartz.nipB1Bolt12Zaps.verify.Bolt12ZapValidation import com.vitorpamplona.quartz.utils.Log @@ -74,12 +74,12 @@ class AccountZapActions( event: Event, pollOption: Int?, message: String = "", - zapType: LnZapEvent.ZapType, + zapType: ZapReceiptEvent.ZapType, toUser: User?, additionalRelays: Set<NormalizedRelayUrl>? = null, amountMillisats: Long? = null, lnurl: String? = null, - ) = LnZapRequestEvent.create( + ) = ZapRequestEvent.create( zappedEvent = event, // Where the provider should publish the receipt. Zapping group content pins that to the room's // host relay: the receipt belongs where the message it pays for lives, so the room can show it @@ -205,7 +205,7 @@ class AccountZapActions( offer: String, amountMillisats: Long, message: String, - zapType: LnZapEvent.ZapType, + zapType: ZapReceiptEvent.ZapType, // (messageResId, detail) — the caller localizes; detail carries a wallet error, if any. onError: (StringResource, String?) -> Unit, // (code, detail) — the wallet refused or failed the payment; no funds moved. @@ -215,7 +215,7 @@ class AccountZapActions( ) { // NONZAP means "pay, but publish no receipt" — settle the offer without binding // a zap intent or emitting a 9736, matching the privacy of a bolt11 NONZAP. - if (zapType == LnZapEvent.ZapType.NONZAP) { + if (zapType == ZapReceiptEvent.ZapType.NONZAP) { sendNwcRequest(PayMethod.create("bitcoin:?lno=$offer", amountMillisats), onTimeout) { response -> account.scope.launch { try { @@ -228,7 +228,7 @@ class AccountZapActions( return } - val anonymous = zapType == LnZapEvent.ZapType.ANONYMOUS + val anonymous = zapType == ZapReceiptEvent.ZapType.ANONYMOUS // The 9737 intent and the 9736 zap MUST be signed by the same key. An anonymous // zap uses a fresh ephemeral key so it carries no `P` tag and isn't traceable. val zapSigner = if (anonymous) NostrSignerInternal(KeyPair()) else account.signer @@ -284,12 +284,12 @@ class AccountZapActions( suspend fun createZapRequestFor( user: User, message: String = "", - zapType: LnZapEvent.ZapType, + zapType: ZapReceiptEvent.ZapType, amountMillisats: Long? = null, lnurl: String? = null, - ): LnZapRequestEvent { + ): ZapRequestEvent { val zapRequest = - LnZapRequestEvent.create( + ZapRequestEvent.create( userHex = user.pubkeyHex, relays = account.nip65RelayList.inboxFlow.value + (user.inboxRelays() ?: emptyList()), signer = account.signer, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/EventBroadcaster.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/EventBroadcaster.kt index 298fff1dff..9765753489 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/EventBroadcaster.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/EventBroadcaster.kt @@ -40,16 +40,16 @@ import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip29RelayGroups.isGroupScoped import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip60Cashu.wallet.CashuWalletEvent import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent @@ -121,7 +121,7 @@ class EventBroadcaster( event is AppSpecificDataEvent || event is BookmarkListEvent || event is OldBookmarkListEvent || - event is LabeledBookmarkListEvent + event is BookmarkSetEvent ) { return false } @@ -166,7 +166,7 @@ class EventBroadcaster( } // Seals, inner DM messages, and unsigned rumors never get broadcast // relays: they only travel inside gift wraps. - if (event is SealedRumorEvent || event is BaseDMGroupEvent || event.sig.isEmpty()) { + if (event is SealEvent || event is BaseDMGroupEvent || event.sig.isEmpty()) { return emptySet() } @@ -322,7 +322,7 @@ class EventBroadcaster( .toList() } val filter = - if (host.kind == SealedRumorEvent.KIND) { + if (host.kind == SealEvent.KIND) { Filter( kinds = listOf(host.kind), ids = listOf(host.id), @@ -383,7 +383,7 @@ class EventBroadcaster( fun sendRestoredVersion(event: Event) { when (event) { is AdvertisedRelayListEvent -> sendEverywhereAnd(event, event.relays().mapTo(mutableSetOf()) { it.relayUrl }) - is ChatMessageRelayListEvent -> sendEverywhereAnd(event, event.relays().toSet()) + is DmRelayListEvent -> sendEverywhereAnd(event, event.relays().toSet()) is KeyPackageRelayListEvent -> sendEverywhereAnd(event, event.relays().toSet()) is CashuWalletEvent -> sendLiterallyEverywhere(event) // Profiles and nutzap info are normally saved everywhere so others can find them; diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/algoFeeds/FavoriteAlgoFeedsOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/algoFeeds/FavoriteAlgoFeedsOrchestrator.kt index 0496ad62dd..e8aa1a078b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/algoFeeds/FavoriteAlgoFeedsOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/algoFeeds/FavoriteAlgoFeedsOrchestrator.kt @@ -25,8 +25,8 @@ import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.status.DvmStatusEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope @@ -60,7 +60,7 @@ data class FavoriteAlgoFeedsSnapshot( val responseRelays: Set<NormalizedRelayUrl> = emptySet(), val ids: Set<HexKey> = emptySet(), val addresses: Set<String> = emptySet(), - val latestStatus: NIP90StatusEvent? = null, + val latestStatus: DvmStatusEvent? = null, val errorMessage: String? = null, ) @@ -139,9 +139,9 @@ class FavoriteAlgoFeedsOrchestrator( launch { account.cache - .observeLatestEvent<NIP90ContentDiscoveryResponseEvent>( + .observeLatestEvent<DvmContentDiscoveryResponseEvent>( Filter( - kinds = listOf(NIP90ContentDiscoveryResponseEvent.KIND), + kinds = listOf(DvmContentDiscoveryResponseEvent.KIND), tags = mapOf("e" to listOf(requestId)), limit = 1, ), @@ -159,9 +159,9 @@ class FavoriteAlgoFeedsOrchestrator( launch { account.cache - .observeLatestEvent<NIP90StatusEvent>( + .observeLatestEvent<DvmStatusEvent>( Filter( - kinds = listOf(NIP90StatusEvent.KIND), + kinds = listOf(DvmStatusEvent.KIND), tags = mapOf("e" to listOf(requestId)), limit = 1, ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt index 360fd7309b..1f97c26b2f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi @@ -48,14 +48,14 @@ class DmRelayListState( // Creates a long-term reference for this note so that the GC doesn't collect the note it self val dmListNote = cache.getOrCreateAddressableNote(getDMRelayListAddress()) - fun getDMRelayListAddress() = ChatMessageRelayListEvent.createAddress(signer.pubKey) + fun getDMRelayListAddress() = DmRelayListEvent.createAddress(signer.pubKey) fun getDMRelayListFlow(): StateFlow<NoteState> = dmListNote.flow().metadata.stateFlow - fun getDMRelayList(): ChatMessageRelayListEvent? = dmListNote.event as? ChatMessageRelayListEvent + fun getDMRelayList(): DmRelayListEvent? = dmListNote.event as? DmRelayListEvent fun normalizeDMRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> { - val event = note.event as? ChatMessageRelayListEvent ?: settings.backupDMRelayList + val event = note.event as? DmRelayListEvent ?: settings.backupDMRelayList return event?.relays()?.toSet() ?: emptySet() } @@ -70,16 +70,16 @@ class DmRelayListState( emptySet(), ) - suspend fun saveRelayList(dmRelays: List<NormalizedRelayUrl>): ChatMessageRelayListEvent { + suspend fun saveRelayList(dmRelays: List<NormalizedRelayUrl>): DmRelayListEvent { val relayListForDMs = getDMRelayList() return if (relayListForDMs != null && relayListForDMs.tags.isNotEmpty()) { - ChatMessageRelayListEvent.updateRelayList( + DmRelayListEvent.updateRelayList( earlierVersion = relayListForDMs, relays = dmRelays, signer = signer, ) } else { - ChatMessageRelayListEvent.create( + DmRelayListEvent.create( relays = dmRelays, signer = signer, ) @@ -99,7 +99,7 @@ class DmRelayListState( Log.d("AccountRegisterObservers", "NIP-17 Relay List Collector Start") getDMRelayListFlow().collect { Log.d("AccountRegisterObservers") { "Updating DM Relay List for ${signer.pubKey}" } - (it.note.event as? ChatMessageRelayListEvent)?.let { + (it.note.event as? DmRelayListEvent)?.let { settings.updateDMRelayList(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPacksState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPacksState.kt index 7021497479..b9af0de585 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPacksState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/OwnedEmojiPacksState.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip01Core.signers.update -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag import com.vitorpamplona.quartz.nip30CustomEmoji.emoji import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent @@ -129,12 +129,12 @@ class OwnedEmojiPacksState( .onStart { emit(listFeedFlow.value.getPack(dTag)) } .flowOn(Dispatchers.IO) - fun DeletionEvent.hasAnyDeletedOwnedEmojiPacks() = deleteAddressesWithKind(EmojiPackEvent.KIND) || deletesAnyEventIn(ownedEmojiPackEventIds.value) + fun DeletionRequestEvent.hasAnyDeletedOwnedEmojiPacks() = deleteAddressesWithKind(EmojiPackEvent.KIND) || deletesAnyEventIn(ownedEmojiPackEventIds.value) fun hasItemInNoteList(notes: Set<Note>): Boolean = notes.anyNotNullEvent { event -> if (event.pubKey == signer.pubKey) { - event is EmojiPackEvent || (event is DeletionEvent && event.hasAnyDeletedOwnedEmojiPacks()) + event is EmojiPackEvent || (event is DeletionRequestEvent && event.hasAnyDeletedOwnedEmojiPacks()) } else { false } @@ -269,7 +269,7 @@ class OwnedEmojiPacksState( account: Account, ) { val packEvent = getOwnedEmojiPackEvent(dTag) ?: return - val deletionEventTemplate = DeletionEvent.build(listOf(packEvent)) + val deletionEventTemplate = DeletionRequestEvent.build(listOf(packEvent)) val deletionEvent = signer.sign(deletionEventTemplate) account.sendMyPublicAndPrivateOutbox(deletionEvent) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt index 2252b83c4f..1c576e1a4f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt @@ -36,10 +36,10 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip47WalletConnect.cache.NostrWalletConnectRequestCache import com.vitorpamplona.quartz.nip47WalletConnect.cache.NostrWalletConnectResponseCache -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcInfoEvent import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcNotificationEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransaction import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaymentReceivedNotification @@ -199,12 +199,12 @@ class NwcSignerState( override fun isNIP47Author(pubKey: HexKey?): Boolean = nip47Signer.value.pubKey == pubKey - override suspend fun decryptRequest(event: LnZapPaymentRequestEvent): Request? { + override suspend fun decryptRequest(event: NwcRequestEvent): Request? { if (!hasWalletConnectSetup()) return null return zapPaymentRequestDecryptionCache.value.decryptRequest(event) } - override suspend fun decryptResponse(event: LnZapPaymentResponseEvent): Response? { + override suspend fun decryptResponse(event: NwcResponseEvent): Response? { if (!hasWalletConnectSetup()) return null return zapPaymentResponseDecryptionCache.value.decryptResponse(event) } @@ -252,7 +252,7 @@ class NwcSignerState( request: Request, onTimeout: () -> Unit = {}, onResponse: (Response?) -> Unit, - ): Pair<LnZapPaymentRequestEvent, NormalizedRelayUrl> = sendNwcRequestToWallet(defaultWalletUri.value, request, onTimeout, onResponse) + ): Pair<NwcRequestEvent, NormalizedRelayUrl> = sendNwcRequestToWallet(defaultWalletUri.value, request, onTimeout, onResponse) /** * Sends a generic NIP-47 request to a specific wallet. @@ -266,14 +266,14 @@ class NwcSignerState( request: Request, onTimeout: () -> Unit = {}, onResponse: (Response?) -> Unit, - ): Pair<LnZapPaymentRequestEvent, NormalizedRelayUrl> { + ): Pair<NwcRequestEvent, NormalizedRelayUrl> { val walletService = walletUri ?: throw IllegalArgumentException("No NIP47 setup") val walletSigner = buildSigner(walletService) ?: signer val info = walletInfo(walletService) request.dropMetadataIfUnsupported(info) - val event = LnZapPaymentRequestEvent.createRequest(request, walletService.pubKeyHex, walletSigner, useNip44 = prefersNip44(info)) + val event = NwcRequestEvent.createRequest(request, walletService.pubKeyHex, walletSigner, useNip44 = prefersNip44(info)) val filter = NWCPaymentQueryState( @@ -314,7 +314,7 @@ class NwcSignerState( onTimeout: () -> Unit = {}, metadata: Map<String, Any?>? = null, onResponse: (Response?) -> Unit, - ): Pair<LnZapPaymentRequestEvent, NormalizedRelayUrl> { + ): Pair<NwcRequestEvent, NormalizedRelayUrl> { val walletService = defaultWalletUri.value ?: throw IllegalArgumentException("No NIP47 setup") val info = walletInfo(walletService) @@ -322,7 +322,7 @@ class NwcSignerState( request.dropMetadataIfUnsupported(info) val event = - LnZapPaymentRequestEvent.createRequest( + NwcRequestEvent.createRequest( request, walletService.pubKeyHex, nip47Signer.value, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt index 92869bfbb5..8ef88a5e24 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip51Lists.hashtagLists.HashtagListDecryptionCache import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi @@ -50,14 +50,14 @@ class HashtagListState( // Creates a long-term reference for this note so that the GC doesn't collect the note it self val hashtagListNote = cache.getOrCreateAddressableNote(getHashtagListAddress()) - fun getHashtagListAddress() = HashtagListEvent.createAddress(signer.pubKey) + fun getHashtagListAddress() = InterestListEvent.createAddress(signer.pubKey) fun getHashtagListFlow(): StateFlow<NoteState> = hashtagListNote.flow().metadata.stateFlow - fun getHashtagList(): HashtagListEvent? = hashtagListNote.event as? HashtagListEvent + fun getHashtagList(): InterestListEvent? = hashtagListNote.event as? InterestListEvent suspend fun hashtagListWithBackup(note: Note): Set<String> { - val event = note.event as? HashtagListEvent ?: settings.backupHashtagList + val event = note.event as? InterestListEvent ?: settings.backupHashtagList return event?.let { decryptionCache.hashtags(it).mapTo(mutableSetOf()) { it.lowercase() } } ?: emptySet() } @@ -75,31 +75,31 @@ class HashtagListState( emptySet(), ) - suspend fun follow(hashtags: List<String>): HashtagListEvent { + suspend fun follow(hashtags: List<String>): InterestListEvent { val hashtagList = getHashtagList() return if (hashtagList == null) { - HashtagListEvent.create(hashtags, true, signer) + InterestListEvent.create(hashtags, true, signer) } else { - HashtagListEvent.add(hashtagList, hashtags, true, signer) + InterestListEvent.add(hashtagList, hashtags, true, signer) } } - suspend fun follow(hashtag: String): HashtagListEvent { + suspend fun follow(hashtag: String): InterestListEvent { val hashtagList = getHashtagList() return if (hashtagList == null) { - HashtagListEvent.create(hashtag.lowercase(), true, signer) + InterestListEvent.create(hashtag.lowercase(), true, signer) } else { - HashtagListEvent.add(hashtagList, hashtag.lowercase(), true, signer) + InterestListEvent.add(hashtagList, hashtag.lowercase(), true, signer) } } - suspend fun unfollow(hashtag: String): HashtagListEvent? { + suspend fun unfollow(hashtag: String): InterestListEvent? { val hashtagList = getHashtagList() return if (hashtagList != null) { - HashtagListEvent.remove(hashtagList, hashtag, signer) + InterestListEvent.remove(hashtagList, hashtag, signer) } else { null } @@ -118,7 +118,7 @@ class HashtagListState( Log.d("AccountRegisterObservers", "Hashtag List Collector Start") getHashtagListFlow().collect { Log.d("AccountRegisterObservers") { "Hashtag List for ${signer.pubKey}" } - (it.note.event as? HashtagListEvent)?.let { + (it.note.event as? InterestListEvent)?.let { settings.updateHashtagListTo(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/interestSets/InterestSetsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/interestSets/InterestSetsState.kt index 487aa8748b..f70f9a13cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/interestSets/InterestSetsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/interestSets/InterestSetsState.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.amethyst.commons.model.nip51Lists.interestSets.Interest import com.vitorpamplona.amethyst.commons.model.updateFlow import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag import kotlinx.coroutines.CoroutineScope @@ -106,12 +106,12 @@ class InterestSetsState( fun getInterestSet(dTag: String) = listFeedFlow.value.getSet(dTag) - fun DeletionEvent.hasAnyDeletedInterestSets() = deleteAddressesWithKind(InterestSetEvent.KIND) || deletesAnyEventIn(interestSetEventIds.value) + fun DeletionRequestEvent.hasAnyDeletedInterestSets() = deleteAddressesWithKind(InterestSetEvent.KIND) || deletesAnyEventIn(interestSetEventIds.value) fun hasItemInNoteList(notes: Set<Note>): Boolean = notes.anyNotNullEvent { event -> if (event.pubKey == signer.pubKey) { - event is InterestSetEvent || (event is DeletionEvent && event.hasAnyDeletedInterestSets()) + event is InterestSetEvent || (event is DeletionRequestEvent && event.hasAnyDeletedInterestSets()) } else { false } @@ -178,7 +178,7 @@ class InterestSetsState( account: Account, ) { val event = getInterestSetEvent(identifier) - val template = DeletionEvent.build(listOf(event)) + val template = DeletionRequestEvent.build(listOf(event)) val deletionEvent = account.signer.sign(template) account.sendMyPublicAndPrivateOutbox(deletionEvent) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt index 71626d7d4e..49a1c8b3d4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt @@ -33,11 +33,11 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.update -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.description import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.image import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.title @@ -62,7 +62,7 @@ class LabeledBookmarkListsState( ) { val user = cache.getOrCreateUser(signer.pubKey) - fun existingLabeledBookmarkNotes() = cache.addressables.filter(LabeledBookmarkListEvent.KIND, user.pubkeyHex) + fun existingLabeledBookmarkNotes() = cache.addressables.filter(BookmarkSetEvent.KIND, user.pubkeyHex) val labeledBookmarkListVersions = MutableStateFlow(0) @@ -81,14 +81,14 @@ class LabeledBookmarkListsState( .stateIn(scope, SharingStarted.Eagerly, emptySet()) @OptIn(ExperimentalCoroutinesApi::class) - val latestBookmarkLists: StateFlow<List<LabeledBookmarkListEvent>> = + val latestBookmarkLists: StateFlow<List<BookmarkSetEvent>> = labeledBookmarkListNotes - .transformLatest { emitAll(it.updateFlow<LabeledBookmarkListEvent>()) } + .transformLatest { emitAll(it.updateFlow<BookmarkSetEvent>()) } .onStart { emit(labeledBookmarkListNotes.value.events()) } .flowOn(Dispatchers.IO) .stateIn(scope, SharingStarted.Eagerly, emptyList()) - suspend fun LabeledBookmarkListEvent.toLabeledBookmarkList() = + suspend fun BookmarkSetEvent.toLabeledBookmarkList() = LabeledBookmarkList( identifier = dTag(), title = titleOrName() ?: dTag(), @@ -98,7 +98,7 @@ class LabeledBookmarkListsState( publicBookmarks = publicBookmarks().toSet(), ) - suspend fun List<LabeledBookmarkListEvent>.toLabeledBookmarkListsFeed() = map { it.toLabeledBookmarkList() }.sortedBy { it.title } + suspend fun List<BookmarkSetEvent>.toLabeledBookmarkListsFeed() = map { it.toLabeledBookmarkList() }.sortedBy { it.title } val listFeedFlow = latestBookmarkLists @@ -114,12 +114,12 @@ class LabeledBookmarkListsState( fun getBookmarkList(dTag: String) = listFeedFlow.value.getList(bookmarkListId = dTag) - fun DeletionEvent.hasAnyDeletedBookmarkLists() = deleteAddressesWithKind(LabeledBookmarkListEvent.KIND) || deletesAnyEventIn(labeledBookmarkListEventIds.value) + fun DeletionRequestEvent.hasAnyDeletedBookmarkLists() = deleteAddressesWithKind(BookmarkSetEvent.KIND) || deletesAnyEventIn(labeledBookmarkListEventIds.value) fun hasItemInNoteList(notes: Set<Note>): Boolean = notes.anyNotNullEvent { event -> if (event.pubKey == signer.pubKey) { - event is LabeledBookmarkListEvent || (event is DeletionEvent && event.hasAnyDeletedBookmarkLists()) + event is BookmarkSetEvent || (event is DeletionRequestEvent && event.hasAnyDeletedBookmarkLists()) } else { false } @@ -143,7 +143,7 @@ class LabeledBookmarkListsState( fun getLabeledBookmarkListNote(bookmarkIdentifier: String): AddressableNote? = existingLabeledBookmarkNotes().find { it.dTag() == bookmarkIdentifier } - fun getLabeledBookmarkListEvent(bookmarkIdentifier: String): LabeledBookmarkListEvent = getLabeledBookmarkListNote(bookmarkIdentifier)?.event as LabeledBookmarkListEvent + fun getLabeledBookmarkListEvent(bookmarkIdentifier: String): BookmarkSetEvent = getLabeledBookmarkListNote(bookmarkIdentifier)?.event as BookmarkSetEvent fun getLabeledBookmarkListFlow(bookmarkIdentifier: String) = listFeedFlow @@ -163,7 +163,7 @@ class LabeledBookmarkListsState( account: Account, ) { val newList = - LabeledBookmarkListEvent.create( + BookmarkSetEvent.create( title = listName, description = listDescription, image = listImage, @@ -202,7 +202,7 @@ class LabeledBookmarkListsState( ) { val listEvent = getLabeledBookmarkListEvent(bookmarkList.identifier) val renamedList = - LabeledBookmarkListEvent.modifyName( + BookmarkSetEvent.modifyName( earlierVersion = listEvent, newTitle = newName, signer = account.signer, @@ -217,7 +217,7 @@ class LabeledBookmarkListsState( ) { val listEvent = getLabeledBookmarkListEvent(bookmarkList.identifier) val modifiedList = - LabeledBookmarkListEvent.modifyDescription( + BookmarkSetEvent.modifyDescription( earlierVersion = listEvent, newDescription = newDescription, signer = account.signer, @@ -232,7 +232,7 @@ class LabeledBookmarkListsState( account: Account, ) { val clonedList = - LabeledBookmarkListEvent.create( + BookmarkSetEvent.create( title = customCloneName ?: currentBookmarkList.title, description = customCloneDescription ?: currentBookmarkList.description, publicBookmarks = currentBookmarkList.publicBookmarks.toList(), @@ -247,7 +247,7 @@ class LabeledBookmarkListsState( account: Account, ) { val listEvent = getLabeledBookmarkListEvent(bookmarkListIdentifier) - val deletionEventTemplate = DeletionEvent.build(listOf(listEvent)) + val deletionEventTemplate = DeletionRequestEvent.build(listOf(listEvent)) val deletionEvent = account.signer.sign(deletionEventTemplate) account.sendMyPublicAndPrivateOutbox(deletionEvent) } @@ -260,7 +260,7 @@ class LabeledBookmarkListsState( ) { val currentBookmarkList = getLabeledBookmarkListEvent(bookmarkListIdentifier) val updatedList = - LabeledBookmarkListEvent.addBookmark( + BookmarkSetEvent.addBookmark( earlierVersion = currentBookmarkList, bookmarkIdTag = bookmark, isPrivate = isBookmarkPrivate, @@ -277,7 +277,7 @@ class LabeledBookmarkListsState( ) { val bookmarkList = getLabeledBookmarkListEvent(bookmarkListIdentifier) val updatedList = - LabeledBookmarkListEvent.moveBookmark( + BookmarkSetEvent.moveBookmark( earlierVersion = bookmarkList, bookmarkIdTag = bookmark, isCurrentlyPrivate = isBookmarkCurrentlyPrivate, @@ -294,7 +294,7 @@ class LabeledBookmarkListsState( ) { val currentBookmarkList = getLabeledBookmarkListEvent(bookmarkListIdentifier) val updatedList = - LabeledBookmarkListEvent.removeBookmark( + BookmarkSetEvent.removeBookmark( earlierVersion = currentBookmarkList, bookmarkIdTag = bookmark, isPrivate = isBookmarkPrivate, @@ -311,7 +311,7 @@ class LabeledBookmarkListsState( ) { if (deletedEventIds.isEmpty() && deletedAddresses.isEmpty()) return - val currentList = getLabeledBookmarkListNote(bookmarkListIdentifier)?.event as? LabeledBookmarkListEvent ?: return + val currentList = getLabeledBookmarkListNote(bookmarkListIdentifier)?.event as? BookmarkSetEvent ?: return val newPublicTags = currentList.tags @@ -328,7 +328,7 @@ class LabeledBookmarkListsState( val updatedList = if (oldPrivateTags == null) { if (newPublicTags.size == currentList.tags.size) return - LabeledBookmarkListEvent.resign( + BookmarkSetEvent.resign( content = currentList.content, tags = newPublicTags, signer = account.signer, @@ -344,7 +344,7 @@ class LabeledBookmarkListsState( } }.toTypedArray() if (newPublicTags.size == currentList.tags.size && newPrivateTags.size == oldPrivateTags.size) return - LabeledBookmarkListEvent.resign( + BookmarkSetEvent.resign( tags = newPublicTags, privateTags = newPrivateTags, signer = account.signer, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt index 0c10d3d49b..fbcdfe4e46 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt @@ -35,8 +35,8 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.update import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip51Lists.followList.description import com.vitorpamplona.quartz.nip51Lists.followList.image import com.vitorpamplona.quartz.nip51Lists.followList.person @@ -79,7 +79,7 @@ class FollowListsState( // them from relays. fun existingPeopleListNotes() = cache.addressables - .filter(FollowListEvent.KIND, user.pubkeyHex) + .filter(StarterPackEvent.KIND, user.pubkeyHex) .filter { it.event != null || !cache.hasBeenDeleted(it.address) } val followListVersions = MutableStateFlow(0) @@ -99,14 +99,14 @@ class FollowListsState( .stateIn(scope, SharingStarted.Eagerly, emptySet()) @OptIn(ExperimentalCoroutinesApi::class) - val latestLists: StateFlow<List<FollowListEvent>> = + val latestLists: StateFlow<List<StarterPackEvent>> = followListNotes - .transformLatest { emitAll(it.updateFlow<FollowListEvent>()) } + .transformLatest { emitAll(it.updateFlow<StarterPackEvent>()) } .onStart { emit(followListNotes.value.events()) } .flowOn(Dispatchers.IO) .stateIn(scope, SharingStarted.Eagerly, emptyList()) - fun List<FollowListEvent>.mapToUserIdSet() = this.map { it.followIdSet() }.flattenToSet() + fun List<StarterPackEvent>.mapToUserIdSet() = this.map { it.followIdSet() }.flattenToSet() val allPeopleListProfiles: StateFlow<Set<HexKey>> = latestLists @@ -115,7 +115,7 @@ class FollowListsState( .flowOn(Dispatchers.IO) .stateIn(scope, SharingStarted.Eagerly, emptySet()) - fun FollowListEvent.toUI() = + fun StarterPackEvent.toUI() = PeopleList( identifierTag = this.dTag(), title = this.title() ?: this.dTag(), @@ -125,7 +125,7 @@ class FollowListsState( publicMembers = cache.load(this.followIdSet()), ) - fun List<FollowListEvent>.toUI() = this.map { it.toUI() } + fun List<StarterPackEvent>.toUI() = this.map { it.toUI() } val uiListFlow = latestLists @@ -148,12 +148,12 @@ class FollowListsState( fun isUserInFollowSets(user: User): Boolean = allPeopleListProfiles.value.contains(user.pubkeyHex) - fun DeletionEvent.hasDeletedAnyFollowList() = deleteAddressesWithKind(FollowListEvent.KIND) || deletesAnyEventIn(followListsEventIds.value) + fun DeletionRequestEvent.hasDeletedAnyFollowList() = deleteAddressesWithKind(StarterPackEvent.KIND) || deletesAnyEventIn(followListsEventIds.value) fun hasItemInNoteList(notes: Set<Note>): Boolean = notes.anyNotNullEvent { event -> if (event.pubKey == signer.pubKey) { - event is FollowListEvent || (event is DeletionEvent && event.hasDeletedAnyFollowList()) + event is StarterPackEvent || (event is DeletionRequestEvent && event.hasDeletedAnyFollowList()) } else { false } @@ -181,7 +181,7 @@ class FollowListsState( fun getPeopleListNote(noteIdentifier: String): AddressableNote? = existingPeopleListNotes().find { it.dTag() == noteIdentifier } - fun getPeopleList(noteIdentifier: String): FollowListEvent = getPeopleListNote(noteIdentifier)?.event as FollowListEvent + fun getPeopleList(noteIdentifier: String): StarterPackEvent = getPeopleListNote(noteIdentifier)?.event as StarterPackEvent fun User.toUserTag() = UserTag(this.pubkeyHex, this.bestRelayHint()) @@ -198,7 +198,7 @@ class FollowListsState( val dTag = UUID.randomUUID().toString() val newListTemplate = - FollowListEvent.build( + StarterPackEvent.build( name = name, people = if (!isPrivate && member != null) listOf(member.toUserTag()) else emptyList(), dTag = dTag, @@ -262,7 +262,7 @@ class FollowListsState( account: Account, ) { val followListEvent = getPeopleList(identifierTag) - val deletionEvent = account.signer.sign(DeletionEvent.build(listOf(followListEvent))) + val deletionEvent = account.signer.sign(DeletionRequestEvent.build(listOf(followListEvent))) account.sendMyPublicAndPrivateOutbox(deletionEvent) // Any screen whose persisted feed filter still points at this follow pack would // keep re-creating an empty shell for its address (and render the dTag/UUID in diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt index 013b2c0a07..bdd1731ab8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt @@ -35,9 +35,9 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.update -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag -import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.description import com.vitorpamplona.quartz.nip51Lists.peopleList.image import com.vitorpamplona.quartz.nip51Lists.peopleList.title @@ -78,8 +78,8 @@ class PeopleListsState( // can subscribe and fetch them from relays. fun existingPeopleListNotes() = cache.addressables - .filter(PeopleListEvent.KIND, user.pubkeyHex) - .filter { it.dTag() != PeopleListEvent.BLOCK_LIST_D_TAG || it.event != null } + .filter(FollowSetEvent.KIND, user.pubkeyHex) + .filter { it.dTag() != FollowSetEvent.BLOCK_LIST_D_TAG || it.event != null } .filter { it.event != null || !cache.hasBeenDeleted(it.address) } val peopleListVersions = MutableStateFlow(0) @@ -99,21 +99,21 @@ class PeopleListsState( .stateIn(scope, SharingStarted.Eagerly, emptySet()) @OptIn(ExperimentalCoroutinesApi::class) - val latestLists: StateFlow<List<PeopleListEvent>> = + val latestLists: StateFlow<List<FollowSetEvent>> = peopleListNotes - .transformLatest { emitAll(it.updateFlow<PeopleListEvent>()) } + .transformLatest { emitAll(it.updateFlow<FollowSetEvent>()) } .onStart { emit(peopleListNotes.value.events()) } .flowOn(Dispatchers.IO) .stateIn(scope, SharingStarted.Eagerly, emptyList()) - suspend fun PeopleListEvent.userIdSet() = decryptionCache.userIdSet(this) + suspend fun FollowSetEvent.userIdSet() = decryptionCache.userIdSet(this) - suspend fun List<PeopleListEvent>.mapToUserIdSet() = this.map { it.userIdSet() }.flattenToSet() + suspend fun List<FollowSetEvent>.mapToUserIdSet() = this.map { it.userIdSet() }.flattenToSet() - suspend fun List<PeopleListEvent>.mapGoodUsersToIdSet() = + suspend fun List<FollowSetEvent>.mapGoodUsersToIdSet() = this .mapNotNull { - if (it.dTag() != PeopleListEvent.BLOCK_LIST_D_TAG) { + if (it.dTag() != FollowSetEvent.BLOCK_LIST_D_TAG) { it.userIdSet() } else { null @@ -127,7 +127,7 @@ class PeopleListsState( .flowOn(Dispatchers.IO) .stateIn(scope, SharingStarted.Eagerly, emptySet()) - suspend fun PeopleListEvent.toUI() = + suspend fun FollowSetEvent.toUI() = PeopleList( identifierTag = this.dTag(), title = this.titleOrName() ?: this.dTag(), @@ -137,7 +137,7 @@ class PeopleListsState( publicMembers = cache.load(this.publicUsersIdSet()), ) - suspend fun List<PeopleListEvent>.toUI() = this.map { it.toUI() }.sortedBy { it.title } + suspend fun List<FollowSetEvent>.toUI() = this.map { it.toUI() }.sortedBy { it.title } val uiListFlow = latestLists @@ -158,12 +158,12 @@ class PeopleListsState( .map { it.select(dTag) } .onStart { emit(selectList(dTag)) } - fun DeletionEvent.hasDeletedAnyPeopleList() = deleteAddressesWithKind(PeopleListEvent.KIND) || deletesAnyEventIn(peopleListsEventIds.value) + fun DeletionRequestEvent.hasDeletedAnyPeopleList() = deleteAddressesWithKind(FollowSetEvent.KIND) || deletesAnyEventIn(peopleListsEventIds.value) fun hasItemInNoteList(notes: Set<Note>): Boolean = notes.anyNotNullEvent { event -> if (event.pubKey == signer.pubKey) { - event is PeopleListEvent || (event is DeletionEvent && event.hasDeletedAnyPeopleList()) + event is FollowSetEvent || (event is DeletionRequestEvent && event.hasDeletedAnyPeopleList()) } else { false } @@ -191,7 +191,7 @@ class PeopleListsState( fun getPeopleListNote(noteIdentifier: String): AddressableNote? = existingPeopleListNotes().find { it.dTag() == noteIdentifier } - fun getPeopleList(noteIdentifier: String): PeopleListEvent = getPeopleListNote(noteIdentifier)?.event as PeopleListEvent + fun getPeopleList(noteIdentifier: String): FollowSetEvent = getPeopleListNote(noteIdentifier)?.event as FollowSetEvent fun User.toUserTag() = UserTag(this.pubkeyHex, this.bestRelayHint()) @@ -207,7 +207,7 @@ class PeopleListsState( ): String { val dTag = UUID.randomUUID().toString() val newListTemplate = - PeopleListEvent.build( + FollowSetEvent.build( dTag = dTag, title = listName, publicMembers = if (!isPrivate && member != null) listOf(member.toUserTag()) else emptyList(), @@ -252,7 +252,7 @@ class PeopleListsState( account: Account, ) { val newList = - PeopleListEvent.createListWithDescription( + FollowSetEvent.createListWithDescription( dTag = UUID.randomUUID().toString(), title = customCloneName ?: currentPeopleList.title, description = customCloneDescription ?: currentPeopleList.description, @@ -268,7 +268,7 @@ class PeopleListsState( account: Account, ) { val followListEvent = getPeopleList(identifierTag) - val deletionEvent = account.signer.sign(DeletionEvent.build(listOf(followListEvent))) + val deletionEvent = account.signer.sign(DeletionRequestEvent.build(listOf(followListEvent))) account.sendMyPublicAndPrivateOutbox(deletionEvent) // Any screen whose persisted feed filter still points at this list would keep // re-creating an empty shell for its address (and render the dTag/UUID in the @@ -284,7 +284,7 @@ class PeopleListsState( ) { val followListEvent = getPeopleList(identifierTag) val newList = - PeopleListEvent.addUser( + FollowSetEvent.addUser( earlierVersion = followListEvent, pubKeyHex = user.pubkeyHex, relayHint = user.bestRelayHint(), @@ -302,7 +302,7 @@ class PeopleListsState( ) { val followListEvent = getPeopleList(identifierTag) val newList = - PeopleListEvent.addUserFirst( + FollowSetEvent.addUserFirst( earlierVersion = followListEvent, pubKeyHex = user.pubkeyHex, relayHint = user.bestRelayHint(), @@ -320,7 +320,7 @@ class PeopleListsState( ) { val followListEvent = getPeopleList(identifierTag) val newList = - PeopleListEvent.removeUser( + FollowSetEvent.removeUser( earlierVersion = followListEvent, pubKeyHex = user.pubkeyHex, isUserPrivate = isPrivate, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayFeeds/RelayFeedListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayFeeds/RelayFeedListState.kt index 7c803a398d..685e8411a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayFeeds/RelayFeedListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayFeeds/RelayFeedListState.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.commons.model.nip51Lists.relayFeeds.RelayFeeds import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi @@ -50,13 +50,13 @@ class RelayFeedListState( // Creates a long-term reference for this note so that the GC doesn't collect the note it self val relayFeedListNote = cache.getOrCreateAddressableNote(getRelayFeedsListAddress()) - fun getRelayFeedsListAddress() = RelayFeedsListEvent.createAddress(signer.pubKey) + fun getRelayFeedsListAddress() = FavoriteRelayListEvent.createAddress(signer.pubKey) fun getRelayFeedsListFlow(): StateFlow<NoteState> = relayFeedListNote.flow().metadata.stateFlow - fun getRelayFeedsList(): RelayFeedsListEvent? = relayFeedListNote.event as? RelayFeedsListEvent + fun getRelayFeedsList(): FavoriteRelayListEvent? = relayFeedListNote.event as? FavoriteRelayListEvent - fun relayFeedsListEvent(note: Note) = note.event as? RelayFeedsListEvent ?: settings.backupRelayFeedsList + fun relayFeedsListEvent(note: Note) = note.event as? FavoriteRelayListEvent ?: settings.backupRelayFeedsList suspend fun normalizeRelayFeedsListWithBackup(note: Note): Set<NormalizedRelayUrl> = relayFeedsListEvent(note)?.let { decryptionCache.relays(it) }?.ifEmpty { null } ?: emptySet() @@ -84,30 +84,30 @@ class RelayFeedListState( emptySet(), ) - suspend fun addRelay(relay: NormalizedRelayUrl): RelayFeedsListEvent { + suspend fun addRelay(relay: NormalizedRelayUrl): FavoriteRelayListEvent { val current = normalizeRelayFeedsListWithBackupNoDefaults(relayFeedListNote).toMutableList() if (relay !in current) current.add(relay) return saveRelayList(current) } - suspend fun removeRelay(relay: NormalizedRelayUrl): RelayFeedsListEvent? { + suspend fun removeRelay(relay: NormalizedRelayUrl): FavoriteRelayListEvent? { val current = normalizeRelayFeedsListWithBackupNoDefaults(relayFeedListNote).toMutableList() if (relay !in current) return null current.remove(relay) return saveRelayList(current) } - suspend fun saveRelayList(relayFeeds: List<NormalizedRelayUrl>): RelayFeedsListEvent { + suspend fun saveRelayList(relayFeeds: List<NormalizedRelayUrl>): FavoriteRelayListEvent { val relayFeedsList = getRelayFeedsList() return if (relayFeedsList != null && relayFeedsList.tags.isNotEmpty()) { - RelayFeedsListEvent.updateRelayList( + FavoriteRelayListEvent.updateRelayList( earlierVersion = relayFeedsList, relays = relayFeeds, signer = signer, ) } else { - RelayFeedsListEvent.create( + FavoriteRelayListEvent.create( relays = relayFeeds, signer = signer, ) @@ -125,7 +125,7 @@ class RelayFeedListState( Log.d("AccountRegisterObservers", "Relay feeds list Collector Start") getRelayFeedsListFlow().collect { Log.d("AccountRegisterObservers") { "Updating Relay feeds list for ${signer.pubKey}" } - (it.note.event as? RelayFeedsListEvent)?.let { + (it.note.event as? FavoriteRelayListEvent)?.let { settings.updateRelayFeedList(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuWalletState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuWalletState.kt index 98a005ebdc..1a97ee66c1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuWalletState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuWalletState.kt @@ -41,7 +41,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchAllPages import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip60Cashu.history.CashuSpendingHistoryEvent import com.vitorpamplona.quartz.nip60Cashu.mintApi.DeterministicSecretFactory import com.vitorpamplona.quartz.nip60Cashu.mintApi.MeltQuoteBolt11ResponseDto @@ -486,7 +486,7 @@ class CashuWalletState( // Process our own NIP-09 deletions inline rather than // relying on LocalCache's `deletedEventBundles`. That - // path only fires when `consume(DeletionEvent)` finds + // path only fires when `consume(DeletionRequestEvent)` finds // the target Note still resident in `notes` — a // LargeSoftCache backed by WeakReferences, which can // be cleared on any GC cycle. When the weak ref is @@ -503,7 +503,7 @@ class CashuWalletState( val ourDeleteIds = all .asSequence() - .filterIsInstance<DeletionEvent>() + .filterIsInstance<DeletionRequestEvent>() .filter { it.pubKey == pubKey } .flatMap { it.deleteEventIds().asSequence() } .toSet() @@ -1447,7 +1447,7 @@ class CashuWalletState( } val redundantIds = redundant.map { it.event.id }.toSet() runCatching { - val template = DeletionEvent.build(redundant.map { it.event }) + val template = DeletionRequestEvent.build(redundant.map { it.event }) val signed = signer.sign(template) publishEvent(signed) }.onFailure { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletConsentSummary.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletConsentSummary.kt index 500b133ef6..850374e92e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletConsentSummary.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletConsentSummary.kt @@ -67,7 +67,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.lightning.LnInvoiceUtil import com.vitorpamplona.quartz.nip01Core.core.fastForEach import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import org.jetbrains.compose.resources.PluralStringResource @@ -199,7 +199,7 @@ class NappletConsentSummary( oneRemoved = Res.string.napplet_consent_diff_unmute_one, ) // Deletions have no prior version to compare against — the tags are the whole request. - DeletionEvent.KIND -> + DeletionRequestEvent.KIND -> pluralFor(Res.plurals.napplet_consent_effect_deletes, countTag(tags, "e") + countTag(tags, "a")) ?.let { Consequence(it) } // Any other kind: at least tell the user tags exist and can be inspected, so an empty diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/gateways/AccountIdentityReader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/gateways/AccountIdentityReader.kt index 369a7b95c9..8bc8f18df3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/gateways/AccountIdentityReader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/gateways/AccountIdentityReader.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.napplet.gateways import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent import kotlinx.serialization.json.add @@ -137,7 +137,7 @@ class AccountIdentityReader( val receipts = cache .filter(Filter(kinds = listOf(9735), tags = mapOf("p" to listOf(pubkey)))) - .mapNotNull { it.event as? LnZapEvent } + .mapNotNull { it.event as? ZapReceiptEvent } return buildJsonArray { receipts.forEach { zap -> addJsonObject { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/V4VPaymentHandler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/V4VPaymentHandler.kt index 0c07fa506f..cb7cc0ef22 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/V4VPaymentHandler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/V4VPaymentHandler.kt @@ -40,7 +40,7 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendMethod import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response import com.vitorpamplona.quartz.nip47WalletConnect.rpc.TlvRecord -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlForm import com.vitorpamplona.quartz.podcasts.PodcastBoostagram import com.vitorpamplona.quartz.podcasts.PodcastValue @@ -91,7 +91,7 @@ class V4VPaymentHandler( onProgress: (percent: Float) -> Unit, onPayInvoicesViaIntent: (invoices: List<String>) -> Unit, asZap: Boolean = false, - zapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PUBLIC, + zapType: ZapReceiptEvent.ZapType = ZapReceiptEvent.ZapType.PUBLIC, ) = withContext(Dispatchers.IO) { val shares = value.computeShares(totalMilliSats) if (shares.isEmpty()) { @@ -190,7 +190,7 @@ class V4VPaymentHandler( shares: List<PodcastValueShare>, message: String, asZap: Boolean, - zapType: LnZapEvent.ZapType, + zapType: ZapReceiptEvent.ZapType, zappedNote: Note?, okHttpClient: (String) -> OkHttpClient, context: Context, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt index 86e3a61284..6c8a9e667c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt @@ -48,8 +48,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcErrorCode import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransactionMetadata import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupLnAddress import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup @@ -79,7 +79,7 @@ class ZapPaymentHandler( // The signed kind 9734 this invoice was fetched with, and the message on it. // Carried so the NWC payment can name the payee (NWC-06 `metadata`); null for // a NONZAP split, which has no zap request to send. - val zapRequest: LnZapRequestEvent? = null, + val zapRequest: ZapRequestEvent? = null, val message: String = "", ) @@ -122,7 +122,7 @@ class ZapPaymentHandler( onError: (String, String, User?) -> Unit, onProgress: (percent: Float) -> Unit, onPayViaIntent: (ImmutableList<Payable>) -> Unit, - zapType: LnZapEvent.ZapType, + zapType: ZapReceiptEvent.ZapType, ) = withContext(Dispatchers.IO) { val noteEvent = note.event val zapSplitSetup = noteEvent?.zapSplitSetup() @@ -297,7 +297,7 @@ class ZapPaymentHandler( note: Note, pollOption: Int?, message: String, - zapType: LnZapEvent.ZapType, + zapType: ZapReceiptEvent.ZapType, totalAmountMilliSats: Long, totalWeight: Double, okHttpClient: (String) -> OkHttpClient, @@ -360,14 +360,14 @@ class ZapPaymentHandler( class ZapRequestReady( val inputSetup: MyZapSplitSetup, - val zapRequest: LnZapRequestEvent?, + val zapRequest: ZapRequestEvent?, ) suspend fun signAllZapRequests( note: Note, pollOption: Int?, message: String, - zapType: LnZapEvent.ZapType, + zapType: ZapReceiptEvent.ZapType, zapsToSend: List<MyZapSplitSetup>, totalAmountMilliSats: Long, // Shared across the lightning + BOLT12 lanes so a mixed split stays proportional. @@ -389,7 +389,7 @@ class ZapPaymentHandler( val splitLnurl = LnurlForm.toUrl(next.lnAddress)?.let(LnurlForm::urlToBech32) val zapRequest = - if (zapType != LnZapEvent.ZapType.NONZAP && noteEvent != null) { + if (zapType != ZapReceiptEvent.ZapType.NONZAP && noteEvent != null) { account.zaps.createZapRequestFor( event = noteEvent, pollOption = pollOption, @@ -535,7 +535,7 @@ class ZapPaymentHandler( totalAmountMilliSats: Long, totalWeight: Double, message: String, - zapType: LnZapEvent.ZapType, + zapType: ZapReceiptEvent.ZapType, okHttpClient: (String) -> OkHttpClient, onError: (String, String, User?) -> Unit, onProgress: (percent: Float) -> Unit, @@ -672,7 +672,7 @@ class ZapPaymentHandler( private suspend fun assembleInvoice( lud16: String, splitSetup: MyZapSplitSetup, - nostrZapRequest: LnZapRequestEvent?, + nostrZapRequest: ZapRequestEvent?, zapValue: Long, message: String, okHttpClient: (String) -> OkHttpClient, @@ -683,7 +683,7 @@ class ZapPaymentHandler( // Only the request the provider actually accepted may be claimed as bound to // this invoice; see lnAddressInvoice's onZapRequestSent. - var sentZapRequest: LnZapRequestEvent? = null + var sentZapRequest: ZapRequestEvent? = null val invoice = LightningAddressResolver().lnAddressInvoice( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt index 83efb8028e..9056561530 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt @@ -38,7 +38,7 @@ import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.service.HttpStatusMessages import com.vitorpamplona.quartz.lightning.LnInvoiceUtil import com.vitorpamplona.quartz.lightning.Lud06 -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointCache import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointInfo import com.vitorpamplona.quartz.utils.Log @@ -128,7 +128,7 @@ class LightningAddressResolver { lnCallback: String, milliSats: Long, message: String, - nostrRequest: LnZapRequestEvent? = null, + nostrRequest: ZapRequestEvent? = null, okHttpClient: (String) -> OkHttpClient, context: Context, ): String { @@ -219,11 +219,11 @@ class LightningAddressResolver { lnAddress: String, milliSats: Long, message: String, - nostrRequest: LnZapRequestEvent? = null, + nostrRequest: ZapRequestEvent? = null, okHttpClient: (String) -> OkHttpClient, onProgress: (percent: Float) -> Unit, context: Context, - onZapRequestSent: (LnZapRequestEvent?) -> Unit = {}, + onZapRequestSent: (ZapRequestEvent?) -> Unit = {}, ): String { val mapper = ObjectMapper() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index 30b71e240a..b596797b6a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -64,7 +64,7 @@ import com.vitorpamplona.quartz.marmot.mip02Welcome.WelcomeEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent @@ -72,7 +72,7 @@ import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent @@ -84,17 +84,17 @@ import com.vitorpamplona.quartz.nip34Git.status.GitStatusAppliedEvent import com.vitorpamplona.quartz.nip34Git.status.GitStatusClosedEvent import com.vitorpamplona.quartz.nip34Git.status.GitStatusDraftEvent import com.vitorpamplona.quartz.nip34Git.status.GitStatusOpenEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent import com.vitorpamplona.quartz.nip64Chess.challenge.accept.LiveChessGameAcceptEvent import com.vitorpamplona.quartz.nip64Chess.move.LiveChessMoveEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallOfferEvent @@ -234,7 +234,7 @@ class EventNotificationConsumer( // public chat the user has silenced (matches the in-app feed, which mutes all four). // Without the second check, muting a channel still let a like on your own message // there notify you — the row's glyph promises silence, so it has to mean it. - if (event is ReactionEvent || event is LnZapEvent || event is RepostEvent || event is GenericRepostEvent) { + if (event is ReactionEvent || event is ZapReceiptEvent || event is RepostEvent || event is GenericRepostEvent) { val target = LocalCache.getNoteIfExists(event)?.replyTo?.lastOrNull() if (target != null && ( @@ -247,7 +247,7 @@ class EventNotificationConsumer( } when (event) { - is PrivateDmEvent -> DirectMessageNotification.notify(applicationContext, account, event) + is EncryptedDmEvent -> DirectMessageNotification.notify(applicationContext, account, event) is ChatMessageEvent -> DirectMessageNotification.notify(applicationContext, account, event) is ChatMessageEncryptedFileHeaderEvent -> DirectMessageNotification.notify(applicationContext, account, event) @@ -256,7 +256,7 @@ class EventNotificationConsumer( is StreamMessageV2Event -> BuzzDmNotification.notify(applicationContext, account, event) is ChatEvent -> BuzzDmNotification.notify(applicationContext, account, event) - is LnZapEvent -> ZapNotification.notify(applicationContext, account, event) + is ZapReceiptEvent -> ZapNotification.notify(applicationContext, account, event) is NutzapEvent -> ZapNotification.notify(applicationContext, account, event) is OnchainZapEvent -> ZapNotification.notify(applicationContext, account, event) @@ -274,15 +274,15 @@ class EventNotificationConsumer( is PictureEvent, is VideoNormalEvent, is VideoShortEvent, - is VideoHorizontalEvent, - is VideoVerticalEvent, + is AddressableNormalVideoEvent, + is AddressableShortVideoEvent, -> MediaNotification.notify(applicationContext, account, event) is PollEvent -> MentionNotification.notify(applicationContext, account, event, titleRes = Res.string.app_notification_poll_channel_message) is HighlightEvent, - is LongTextNoteEvent, - is WikiNoteEvent, + is LongFormContentEvent, + is WikiArticleEvent, -> ArticleNotification.notify(applicationContext, account, event) is GitIssueEvent -> CodeNotification.notify(applicationContext, account, event) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationContent.kt index 238b20c0bc..68077c7d66 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationContent.kt @@ -27,11 +27,11 @@ import com.vitorpamplona.amethyst.commons.model.nip71Video.selectVideoTrack import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoEvent @@ -177,7 +177,7 @@ object NotificationContent { } suspend fun decryptZapContentAuthor( - event: LnZapRequestEvent, + event: ZapRequestEvent, signer: NostrSigner, ): Event? = if (event.isPrivateZap() && event.zappedAuthor().contains(event.pubKey)) { @@ -191,8 +191,8 @@ object NotificationContent { signer: NostrSigner, ): String? = when (val event = note.event) { - is PrivateDmEvent -> event.decryptContent(signer) - is LnZapRequestEvent -> decryptZapContentAuthor(event, signer)?.content + is EncryptedDmEvent -> event.decryptContent(signer) + is ZapRequestEvent -> decryptZapContentAuthor(event, signer)?.content else -> event?.content } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationDispatcher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationDispatcher.kt index f75661b6f3..e9bca8f358 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationDispatcher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationDispatcher.kt @@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.marmot.mip02Welcome.WelcomeEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent @@ -40,7 +40,7 @@ import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent @@ -52,17 +52,17 @@ import com.vitorpamplona.quartz.nip34Git.status.GitStatusAppliedEvent import com.vitorpamplona.quartz.nip34Git.status.GitStatusClosedEvent import com.vitorpamplona.quartz.nip34Git.status.GitStatusDraftEvent import com.vitorpamplona.quartz.nip34Git.status.GitStatusOpenEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent import com.vitorpamplona.quartz.nip64Chess.challenge.accept.LiveChessGameAcceptEvent import com.vitorpamplona.quartz.nip64Chess.move.LiveChessMoveEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallOfferEvent @@ -118,8 +118,8 @@ class NotificationDispatcher( internal val NOTIFICATION_KINDS: Set<Int> = setOf( // Direct-arrival - PrivateDmEvent.KIND, - LnZapEvent.KIND, + EncryptedDmEvent.KIND, + ZapReceiptEvent.KIND, NutzapEvent.KIND, OnchainZapEvent.KIND, Bolt12ZapEvent.KIND, @@ -133,8 +133,8 @@ class NotificationDispatcher( PictureEvent.KIND, VideoNormalEvent.KIND, VideoShortEvent.KIND, - VideoHorizontalEvent.KIND, - VideoVerticalEvent.KIND, + AddressableNormalVideoEvent.KIND, + AddressableShortVideoEvent.KIND, ChannelMessageEvent.KIND, PollEvent.KIND, GitPatchEvent.KIND, @@ -150,8 +150,8 @@ class NotificationDispatcher( GitStatusClosedEvent.KIND, GitStatusDraftEvent.KIND, HighlightEvent.KIND, - LongTextNoteEvent.KIND, - WikiNoteEvent.KIND, + LongFormContentEvent.KIND, + WikiArticleEvent.KIND, LiveChessGameAcceptEvent.KIND, LiveChessMoveEvent.KIND, WakeUpEvent.KIND, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/DirectMessageNotification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/DirectMessageNotification.kt index 23a67fc369..6935da0691 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/DirectMessageNotification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/DirectMessageNotification.kt @@ -32,7 +32,7 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.Conver import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.ReplyAction import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postConversation import com.vitorpamplona.amethyst.service.notifications.notificationManager -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent @@ -64,7 +64,7 @@ object DirectMessageNotification { suspend fun notify( context: Context, account: Account, - event: PrivateDmEvent, + event: EncryptedDmEvent, ) { if (account.signer.pubKey != event.verifiedRecipientPubKey()) return notifyRoom(context, account, event.id, event.createdAt, event.chatroomKey(account.signer.pubKey), decrypt = true) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ZapNotification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ZapNotification.kt index f60e54aad7..6f13908502 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ZapNotification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ZapNotification.kt @@ -39,8 +39,8 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postStandard import com.vitorpamplona.amethyst.service.notifications.notificationManager import com.vitorpamplona.amethyst.ui.note.showAmount -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent import org.jetbrains.compose.resources.StringResource @@ -59,7 +59,7 @@ object ZapNotification { suspend fun notify( context: Context, account: Account, - event: LnZapEvent, + event: ZapReceiptEvent, ) { LocalCache.getNoteIfExists(event.id) ?: return val zapRequestNote = event.zapRequest?.id?.let { LocalCache.checkGetOrCreateNote(it) } ?: return @@ -67,7 +67,7 @@ object ZapNotification { if (!account.isAcceptable(zappedNote)) return if ((event.amount ?: BigDecimal.ZERO) < MIN_ZAP_AMOUNT) return - val zapRequestEvent = zapRequestNote.event as? LnZapRequestEvent ?: return + val zapRequestEvent = zapRequestNote.event as? ZapRequestEvent ?: return // Resolve the (possibly private) zapper once — never re-decrypt per tick. val decrypted = NotificationContent.decryptZapContentAuthor(zapRequestEvent, account.signer) ?: return val sender = LocalCache.getOrCreateUser(decrypted.pubKey) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt index 087e2cdeee..75a9090d82 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt @@ -32,18 +32,18 @@ import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent -import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent +import com.vitorpamplona.quartz.nip38UserStatus.UserStatusEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent import com.vitorpamplona.quartz.nip53LiveActivities.nestsServers.NestsServersEvent @@ -60,9 +60,9 @@ val AccountInfoAndListsFromKeyKinds = listOf( MetadataEvent.KIND, ContactListEvent.KIND, - StatusEvent.KIND, + UserStatusEvent.KIND, AdvertisedRelayListEvent.KIND, - ChatMessageRelayListEvent.KIND, + DmRelayListEvent.KIND, KeyPackageRelayListEvent.KIND, SearchRelayListEvent.KIND, FileServersEvent.KIND, @@ -78,12 +78,12 @@ val AccountInfoAndListsFromKeyKinds2 = BroadcastRelayListEvent.KIND, IndexerRelayListEvent.KIND, ProxyRelayListEvent.KIND, - HashtagListEvent.KIND, + InterestListEvent.KIND, GeohashListEvent.KIND, TrustProviderListEvent.KIND, PaymentTargetsEvent.KIND, Bolt12OfferListEvent.KIND, - RelayFeedsListEvent.KIND, + FavoriteRelayListEvent.KIND, InterestSetEvent.KIND, // NIP-51 "simple groups" list (kind 10009): the user's joined NIP-29 groups + servers. // Loaded up-front so "My Groups" and group memberships resolve immediately at login, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt index 065304ec16..6f7d9e6064 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaym import com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect.NWCPaymentQueryState import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent @SuppressLint("StateFlowValueCalledInComposition") @Composable @@ -50,7 +50,7 @@ fun NWCFinderFilterAssemblerSubscription( val states = remember(note) { val zapPaymentRequestNote = note - (zapPaymentRequestNote.event as? LnZapPaymentRequestEvent)?.let { noteEvent -> + (zapPaymentRequestNote.event as? NwcRequestEvent)?.let { noteEvent -> noteEvent.walletServicePubKey()?.let { zapPaymentRequestNote.relays.map { NWCPaymentQueryState( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index 691179b7e7..bd26b5ad5a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -39,7 +39,7 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -132,9 +132,9 @@ fun observeUserTagFollowCount( .metadata.stateFlow .sample(1000) .mapLatest { noteState -> - (noteState.note.event as? HashtagListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.size ?: 0 + (noteState.note.event as? InterestListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.size ?: 0 }.onStart { - emit((accountViewModel.hashtagFollows(user).event as? HashtagListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.size ?: 0) + emit((accountViewModel.hashtagFollows(user).event as? InterestListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.size ?: 0) }.distinctUntilChanged() .flowOn(Dispatchers.IO) } @@ -160,9 +160,9 @@ fun observeUserTagFollows( .metadata.stateFlow .sample(200) .mapLatest { noteState -> - (noteState.note.event as? HashtagListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.sorted() ?: emptyList() + (noteState.note.event as? InterestListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.sorted() ?: emptyList() }.onStart { - emit((accountViewModel.hashtagFollows(user).event as? HashtagListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.sorted() ?: emptyList()) + emit((accountViewModel.hashtagFollows(user).event as? InterestListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.sorted() ?: emptyList()) }.distinctUntilChanged() .flowOn(Dispatchers.IO) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/MeteringNostrSigner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/MeteringNostrSigner.kt index 93bb31f4a5..1b5355efef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/MeteringNostrSigner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/MeteringNostrSigner.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip46RemoteSigner.signer.NostrSignerRemote import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip89AppHandlers.clientTag.NostrSignerWithClientTag /** @@ -89,7 +89,7 @@ class MeteringNostrSigner( fromPublicKey: HexKey, ): String = metered(UsageKeys.DECRYPT_COUNT, UsageKeys.DECRYPT_US) { inner.nip44Decrypt(ciphertext, fromPublicKey) } - override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent = metered(UsageKeys.DECRYPT_COUNT, UsageKeys.DECRYPT_US) { inner.decryptZapEvent(event) } + override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = metered(UsageKeys.DECRYPT_COUNT, UsageKeys.DECRYPT_US) { inner.decryptZapEvent(event) } override suspend fun deriveKey(nonce: HexKey): HexKey = inner.deriveKey(nonce) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsVideoEventBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsVideoEventBuilder.kt index 6a326f99c9..fe15bfee85 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsVideoEventBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsVideoEventBuilder.kt @@ -26,9 +26,9 @@ import com.davotoula.lightcompressor.hls.HlsUploaded import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoMeta -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip71Video.duration import com.vitorpamplona.quartz.nip71Video.title import com.vitorpamplona.quartz.nip71Video.videoIMetas @@ -61,11 +61,11 @@ data class HlsVideoPublishInput( sealed class HlsVideoEventTemplate { data class Horizontal( - val template: EventTemplate<VideoHorizontalEvent>, + val template: EventTemplate<AddressableNormalVideoEvent>, ) : HlsVideoEventTemplate() data class Vertical( - val template: EventTemplate<VideoVerticalEvent>, + val template: EventTemplate<AddressableShortVideoEvent>, ) : HlsVideoEventTemplate() } @@ -80,7 +80,7 @@ data class HlsBuiltTemplate( ) /** - * Assembles a NIP-71 VideoHorizontalEvent / VideoVerticalEvent template from an HLS upload + * Assembles a NIP-71 AddressableNormalVideoEvent / AddressableShortVideoEvent template from an HLS upload * result. Orientation is decided from the first rendition's width/height: portrait * (height > width) selects kind 34236, otherwise 34235. * @@ -142,7 +142,7 @@ object HlsVideoEventBuilder { val template = if (isVertical) { HlsVideoEventTemplate.Vertical( - VideoVerticalEvent.build(input.description, dTag, createdAt) { + AddressableShortVideoEvent.build(input.description, dTag, createdAt) { videoIMetas(videoMetas) title(input.title) input.durationSeconds?.let { duration(it) } @@ -151,7 +151,7 @@ object HlsVideoEventBuilder { ) } else { HlsVideoEventTemplate.Horizontal( - VideoHorizontalEvent.build(input.description, dTag, createdAt) { + AddressableNormalVideoEvent.build(input.description, dTag, createdAt) { videoIMetas(videoMetas) title(input.title) input.durationSeconds?.let { duration(it) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt index 650224ae95..2b0d73753f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt @@ -54,7 +54,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.uriToRoute import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip92IMeta.imetasByUrl import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking @@ -425,7 +425,7 @@ fun RenderContentAsMarkdownNotePreview() { runBlocking { withContext(Dispatchers.IO) { val blogPost = - LongTextNoteEvent( + LongFormContentEvent( id = "e69fa72a221d0b7362f8e63fd6ab84cc6b5e7b505fe49cefe6823a47a6a4b583", pubKey = "88cc134b1a65f54ef48acc1df3665063d3ea45f04eab8af4646e561c5ae99079", createdAt = 1718785725, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt index 9c36e806a9..79d22a9dbe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasMoreHashtagsThan import com.vitorpamplona.quartz.nip10Notes.threadRootIdOrSelf import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.utils.TimeUtils class FilterByListParams( @@ -105,7 +105,7 @@ class FilterByListParams( fun showHiddenKey( selectedListName: String, userHex: String, - ) = selectedListName == PeopleListEvent.blockListFor(userHex) || selectedListName == MuteListEvent.blockListFor(userHex) + ) = selectedListName == FollowSetEvent.blockListFor(userHex) || selectedListName == MuteListEvent.blockListFor(userHex) fun create( followLists: IFeedTopNavFilter?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/PrefetchFeedMedia.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/PrefetchFeedMedia.kt index 1fbdc53637..3d70cf26d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/PrefetchFeedMedia.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/PrefetchFeedMedia.kt @@ -298,7 +298,7 @@ private class WarmTargets { /** * Adds NIP-94 file-header media: blobs declared as top-level `url` tags (not - * imeta), as used by FileHeaderEvent and the gallery/file kinds. Only image + * imeta), as used by FileMetadataEvent and the gallery/file kinds. Only image * files are prefetched — detected by the URL extension or the event's top-level * `m` mime type (a hashed URL may carry no extension). The decode listener * records their dims like any other image. @@ -333,7 +333,7 @@ private fun Note.collectWarmTargets(): WarmTargets? { } // NIP-94 file-header convention: the blob is a top-level `url` tag with sibling - // `m`/`dim`, not imeta (FileHeaderEvent and the gallery/file kinds). The generic + // `m`/`dim`, not imeta (FileMetadataEvent and the gallery/file kinds). The generic // imetas() above misses these entirely. targets.addFileHeaderMedia(ev) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt index 13f0e95cf2..3962bce3ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt @@ -44,7 +44,7 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent @@ -57,12 +57,12 @@ import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestEvent import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestUpdateEvent import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip59Giftwrap.HasInnerEvent -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent @@ -247,7 +247,7 @@ fun routeForInner( } } - is FollowListEvent -> { + is StarterPackEvent -> { Route.FollowPack(noteEvent.address()) } @@ -294,7 +294,7 @@ fun routeForInner( Route.GeocacheHunt(noteEvent.kind, noteEvent.pubKey, noteEvent.dTag()) } - is GiftWrapEvent, is SealedRumorEvent -> { + is GiftWrapEvent, is SealEvent -> { val wrap = noteEvent as HasInnerEvent wrap.innerEventId?.let { routeFor(LocalCache.getOrCreateNote(it), loggedIn) @@ -456,7 +456,7 @@ fun routeReplyTo( } is ChatroomKeyable -> { - // Covers PrivateDmEvent (NIP-04) and BaseDMGroupEvent (NIP-17) — both + // Covers EncryptedDmEvent (NIP-04) and BaseDMGroupEvent (NIP-17) — both // implement ChatroomKeyable with identical routing semantics here. routeToMessage( room = noteEvent.chatroomKey(account.userProfile().pubkeyHex), @@ -477,7 +477,7 @@ fun routeReplyTo( } } - is LnZapEvent -> { + is ZapReceiptEvent -> { // A public reply can't tag a private zapper without exposing them. // When we hold the decrypted sender (we are the zap recipient), reply // in their DM room instead of the public comment composer. @@ -532,7 +532,7 @@ suspend fun routeEditDraftTo( Route.NewShortNote(draft = note.idHex) } - is LongTextNoteEvent -> { + is LongFormContentEvent -> { Route.NewLongFormPost(draft = note.idHex) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index ee0acbe5e7..208b2c4ba1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -84,7 +84,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.firstTaggedEvent import com.vitorpamplona.quartz.nip01Core.tags.people.firstTaggedUserId import com.vitorpamplona.quartz.nip30CustomEmoji.taggedEmojis -import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent +import com.vitorpamplona.quartz.nip38UserStatus.UserStatusEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentMapOf @@ -219,14 +219,14 @@ fun DisplayStatus( nav: INav, ) { val noteState by observeNote(addressableNote, accountViewModel) - val noteEvent = noteState.note.event as? StatusEvent ?: return + val noteEvent = noteState.note.event as? UserStatusEvent ?: return DisplayStatus(noteEvent, accountViewModel, nav) } @Composable fun DisplayStatus( - event: StatusEvent, + event: UserStatusEvent, accountViewModel: AccountViewModel, nav: INav, ) { @@ -266,7 +266,7 @@ fun DisplayStatusInner( nav: INav, ) { when (type) { - StatusEvent.MUSIC -> { + UserStatusEvent.MUSIC -> { Icon( imageVector = CustomHashTagIcons.Tunestr, null, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 08ae330cde..a05755b51f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -331,16 +331,16 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip13Pow.strongPoWOrNull import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip25Reactions.ExternalReactionEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent @@ -368,26 +368,26 @@ import com.vitorpamplona.quartz.nip51Lists.articleCurationSet.ArticleCurationSet import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent import com.vitorpamplona.quartz.nip51Lists.gitAuthorList.GitAuthorListEvent import com.vitorpamplona.quartz.nip51Lists.gitRepositoryList.GitRepositoryListEvent import com.vitorpamplona.quartz.nip51Lists.goodWikiAuthorList.GoodWikiAuthorListEvent import com.vitorpamplona.quartz.nip51Lists.goodWikiRelayList.GoodWikiRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent import com.vitorpamplona.quartz.nip51Lists.kindMuteSet.KindMuteSetEvent -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent import com.vitorpamplona.quartz.nip51Lists.mediaFollowList.MediaFollowListEvent import com.vitorpamplona.quartz.nip51Lists.mediaStarterPack.MediaStarterPackEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.pictureCurationSet.PictureCurationSetEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent @@ -395,19 +395,19 @@ import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent import com.vitorpamplona.quartz.nip51Lists.videoCurationSet.VideoCurationSetEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import com.vitorpamplona.quartz.nip52Calendar.rsvp.CalendarRSVPEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip54Wiki.WikiMergeAcceptanceEvent import com.vitorpamplona.quartz.nip54Wiki.WikiMergeRequestEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip54Wiki.WikiRedirectEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip57Zaps.splits.hasZapSplitSetupBesidesAuthor import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent @@ -427,7 +427,7 @@ import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprov import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip72ModCommunities.isACommunityPost -import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent +import com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent @@ -437,9 +437,9 @@ import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEven import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.status.DvmStatusEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent import com.vitorpamplona.quartz.nipA4PublicMessages.PublicMessageEvent @@ -1145,7 +1145,7 @@ private fun RenderNoteRow( RenderReport(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) } - is LongTextNoteEvent -> { + is LongFormContentEvent -> { RenderLongFormContent(baseNote, accountViewModel, nav) } @@ -1171,7 +1171,7 @@ private fun RenderNoteRow( RenderCodeSnippetEvent(baseNote) } - is WikiNoteEvent -> { + is WikiArticleEvent -> { RenderWikiContent(baseNote, accountViewModel, nav) } @@ -1191,7 +1191,7 @@ private fun RenderNoteRow( BadgeDisplay(baseNote = baseNote, accountViewModel = accountViewModel, nav = nav) } - is LnZapEvent -> { + is ZapReceiptEvent -> { RenderLnZap(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) } @@ -1215,11 +1215,11 @@ private fun RenderNoteRow( RenderFhirResource(baseNote, accountViewModel, nav) } - is PeopleListEvent -> { + is FollowSetEvent -> { DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav) } - is FollowListEvent -> { + is StarterPackEvent -> { DisplayFollowList(baseNote, true, accountViewModel, nav) } @@ -1231,7 +1231,7 @@ private fun RenderNoteRow( DisplayRelaySet(baseNote, backgroundColor, accountViewModel, nav) } - is ChatMessageRelayListEvent -> { + is DmRelayListEvent -> { DisplayDMRelayList(baseNote, backgroundColor, accountViewModel, nav) } @@ -1251,7 +1251,7 @@ private fun RenderNoteRow( DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav) } - is RelayFeedsListEvent -> { + is FavoriteRelayListEvent -> { DisplayRelayFeedsList(baseNote, backgroundColor, accountViewModel, nav) } @@ -1384,7 +1384,7 @@ private fun RenderNoteRow( ) } - is PrivateDmEvent -> { + is EncryptedDmEvent -> { RenderPrivateMessage( baseNote, makeItShort, @@ -1494,7 +1494,7 @@ private fun RenderNoteRow( RenderCalendarDateSlotEvent(baseNote, accountViewModel, nav) } - is CalendarEvent -> { + is CalendarCollectionEvent -> { RenderCalendarCollectionEvent(baseNote, accountViewModel, nav) } @@ -1502,7 +1502,7 @@ private fun RenderNoteRow( RenderCalendarRSVPEvent(baseNote, accountViewModel, nav) } - is GoalEvent -> { + is ZapGoalEvent -> { RenderGoal(baseNote, accountViewModel, nav) } @@ -1550,7 +1550,7 @@ private fun RenderNoteRow( ) } - is NIP90ContentDiscoveryResponseEvent -> { + is DvmContentDiscoveryResponseEvent -> { RenderNIP90ContentDiscoveryResponse( baseNote, makeItShort, @@ -1562,7 +1562,7 @@ private fun RenderNoteRow( ) } - is NIP90StatusEvent -> { + is DvmStatusEvent -> { RenderNIP90Status( baseNote, accountViewModel, @@ -1640,7 +1640,7 @@ private fun RenderNoteRow( ) } - is FileHeaderEvent -> { + is FileMetadataEvent -> { FileHeaderDisplay(baseNote, true, ContentScale.FillWidth, accountViewModel) } @@ -1879,7 +1879,7 @@ private fun RenderNoteRow( RenderOldBookmarkList(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) } - is LabeledBookmarkListEvent -> { + is BookmarkSetEvent -> { RenderLabeledBookmarkList(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) } @@ -1923,7 +1923,7 @@ private fun RenderNoteRow( RenderMediaStarterPack(baseNote, backgroundColor, accountViewModel, nav) } - is HashtagListEvent -> { + is InterestListEvent -> { RenderHashtagList(baseNote, backgroundColor, accountViewModel, nav) } @@ -2172,7 +2172,7 @@ fun FirstUserInfoRow( // Zap receipts are signed by the recipient's lightning provider; show the // sender from the embedded zap request instead of the service key. val zapSender = - if (baseNote.event is LnZapEvent) { + if (baseNote.event is ZapReceiptEvent) { observeZapSender(baseNote, accountViewModel).value } else { null @@ -2411,7 +2411,7 @@ fun RenderAuthorImages( } else { NoteAuthorPicture(baseNote, Size55dp, accountViewModel = accountViewModel, nav = nav) } - } else if (noteEvent is LnZapEvent) { + } else if (noteEvent is ZapReceiptEvent) { // Zap receipts are signed by the recipient's lightning provider; show the // sender from the embedded zap request instead of the service key, matching // how the thread's master note resolves the author. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index 5cb7a5d1cd..a6999b790b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -116,8 +116,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.report.ReportNoteDialog import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import kotlinx.coroutines.launch import org.jetbrains.compose.resources.StringResource @@ -149,8 +149,8 @@ val externalLinkForNote = { note: Note -> if (note is AddressableNote) { when { note.event?.bountyBaseReward() != null -> "https://nostrbounties.com/b/${note.toNAddr()}" - note.event is PeopleListEvent -> "https://listr.lol/a/${note.toNAddr()}" - note.event is FollowListEvent -> "https://following.space/d/${note.address.dTag}?p=${note.address.pubKeyHex}" + note.event is FollowSetEvent -> "https://listr.lol/a/${note.toNAddr()}" + note.event is StarterPackEvent -> "https://following.space/d/${note.address.dTag}?p=${note.address.pubKeyHex}" else -> njumpLink(note.toNAddr()) } } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt index 7fc4cc5be5..5faaa6a2e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt @@ -95,7 +95,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag -import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiListEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -361,7 +361,7 @@ private fun EmojiSelector( accountViewModel.account.emoji.getEmojiPackSelectionAddress(), ) { emptyNote -> emptyNote?.let { usersEmojiList -> - val collections by observeNoteEventAndMapNotNull(usersEmojiList, accountViewModel) { event: EmojiPackSelectionEvent -> + val collections by observeNoteEventAndMapNotNull(usersEmojiList, accountViewModel) { event: EmojiListEvent -> event.emojiPacks().toImmutableList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index 51351dac5b..af65ce3dbb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -121,7 +121,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.getFragmentActivity import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch @@ -153,22 +153,22 @@ fun UpdateZapAmountContent( val zapTypes = listOf( Triple( - LnZapEvent.ZapType.PUBLIC, + ZapReceiptEvent.ZapType.PUBLIC, stringRes(id = Res.string.zap_type_public), stringRes(id = Res.string.zap_type_public_explainer), ), Triple( - LnZapEvent.ZapType.PRIVATE, + ZapReceiptEvent.ZapType.PRIVATE, stringRes(id = Res.string.zap_type_private), stringRes(id = Res.string.zap_type_private_explainer), ), Triple( - LnZapEvent.ZapType.ANONYMOUS, + ZapReceiptEvent.ZapType.ANONYMOUS, stringRes(id = Res.string.zap_type_anonymous), stringRes(id = Res.string.zap_type_anonymous_explainer), ), Triple( - LnZapEvent.ZapType.NONZAP, + ZapReceiptEvent.ZapType.NONZAP, stringRes(id = Res.string.zap_type_nonzap), stringRes(id = Res.string.zap_type_nonzap_explainer), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountViewModel.kt index b1b44291e6..168e937299 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountViewModel.kt @@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.decodePrivateKeyAsHexOrNull import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import kotlinx.coroutines.CancellationException @Stable @@ -44,7 +44,7 @@ class UpdateZapAmountViewModel : ViewModel() { var walletConnectRelay by mutableStateOf(TextFieldValue("")) var walletConnectPubkey by mutableStateOf(TextFieldValue("")) var walletConnectSecret by mutableStateOf(TextFieldValue("")) - var selectedZapType by mutableStateOf(LnZapEvent.ZapType.PRIVATE) + var selectedZapType by mutableStateOf(ZapReceiptEvent.ZapType.PRIVATE) // A local UI preference rather than synced account state, but it is edited on // this screen, so it follows this screen's Save/Cancel contract instead of diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index 75a78c8df2..ac414ad2b2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -117,7 +117,7 @@ import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.OnchainZapSendDialog import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CancellationException @@ -163,35 +163,35 @@ fun ZapCustomDialog( val zapTypes = listOf( Triple( - LnZapEvent.ZapType.PUBLIC, + ZapReceiptEvent.ZapType.PUBLIC, stringRes(id = Res.string.zap_type_public), stringRes(id = Res.string.zap_type_public_explainer), ), Triple( - LnZapEvent.ZapType.PRIVATE, + ZapReceiptEvent.ZapType.PRIVATE, stringRes(id = Res.string.zap_type_private), stringRes(id = Res.string.zap_type_private_explainer), ), Triple( - LnZapEvent.ZapType.ANONYMOUS, + ZapReceiptEvent.ZapType.ANONYMOUS, stringRes(id = Res.string.zap_type_anonymous), stringRes(id = Res.string.zap_type_anonymous_explainer), ), Triple( - LnZapEvent.ZapType.NONZAP, + ZapReceiptEvent.ZapType.NONZAP, stringRes(id = Res.string.zap_type_nonzap), stringRes(id = Res.string.zap_type_nonzap_explainer), ), ).filter { - !isPrivateTarget || it.first == LnZapEvent.ZapType.PRIVATE || it.first == LnZapEvent.ZapType.NONZAP + !isPrivateTarget || it.first == ZapReceiptEvent.ZapType.PRIVATE || it.first == ZapReceiptEvent.ZapType.NONZAP } var selectedZapType by remember(accountViewModel, baseNote) { val default = accountViewModel.defaultZapType() mutableStateOf( - if (isPrivateTarget && default != LnZapEvent.ZapType.NONZAP) { - LnZapEvent.ZapType.PRIVATE + if (isPrivateTarget && default != ZapReceiptEvent.ZapType.NONZAP) { + ZapReceiptEvent.ZapType.PRIVATE } else { default }, @@ -327,15 +327,15 @@ fun ZapCustomDialog( OutlinedTextField( label = { when (selectedZapType) { - LnZapEvent.ZapType.PUBLIC, LnZapEvent.ZapType.ANONYMOUS -> { + ZapReceiptEvent.ZapType.PUBLIC, ZapReceiptEvent.ZapType.ANONYMOUS -> { Text(text = stringRes(id = Res.string.custom_zaps_add_a_message)) } - LnZapEvent.ZapType.PRIVATE -> { + ZapReceiptEvent.ZapType.PRIVATE -> { Text(text = stringRes(id = Res.string.custom_zaps_add_a_message_private)) } - LnZapEvent.ZapType.NONZAP -> { + ZapReceiptEvent.ZapType.NONZAP -> { Text(text = stringRes(id = Res.string.custom_zaps_add_a_message_nonzap)) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNote.kt index 445efa5a78..60899cf0e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNote.kt @@ -113,7 +113,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockVitorAccountViewModel import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -145,7 +145,7 @@ fun ZapZapPollNotePreview() { ) val zapVote = - LnZapEvent( + ZapReceiptEvent( id = "2a17fdcd0e387d1623c7313d7aa2848e18dde8a942cfe8a2d6b686ea5f68f01a", pubKey = "79f00d3f5a19ec806189fcab03c1be4ff81d18ee4f653c88fac41fe03570f432", createdAt = 1729186293, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNoteViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNoteViewModel.kt index 1a331eb8ac..d8970b6b23 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNoteViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNoteViewModel.kt @@ -30,8 +30,8 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -233,9 +233,9 @@ class PollNoteViewModel : ViewModel() { user: User, ): Boolean = pollNote!!.zaps.any { - val zapEvent = it.value?.event as? LnZapEvent + val zapEvent = it.value?.event as? ZapReceiptEvent val privateZapAuthor = - (it.key.event as? LnZapRequestEvent)?.let { + (it.key.event as? ZapRequestEvent)?.let { account.privateZapsDecryptionCache.cachedPrivateZap(it) } zapEvent?.zappedPollOption() == option && @@ -244,7 +244,7 @@ class PollNoteViewModel : ViewModel() { private fun zappedPollOptionAmount(option: Int): BigDecimal = pollNote?.zaps?.values?.sumOf { - val event = it?.event as? LnZapEvent + val event = it?.event as? ZapReceiptEvent val zapAmount = event?.amount ?: BigDecimal.ZERO val isValidAmount = isValidInputVoteAmount(event?.amount) @@ -258,7 +258,7 @@ class PollNoteViewModel : ViewModel() { private fun totalZapped(): BigDecimal = pollNote?.zaps?.values?.sumOf { - val zapEvent = (it?.event as? LnZapEvent) + val zapEvent = (it?.event as? ZapReceiptEvent) val zapAmount = zapEvent?.amount ?: BigDecimal.ZERO val isValidAmount = isValidInputVoteAmount(zapEvent?.amount) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/WatchAndLoadMyEmojiList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/WatchAndLoadMyEmojiList.kt index 1687f0a6ea..cabe2aafbe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/WatchAndLoadMyEmojiList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/WatchAndLoadMyEmojiList.kt @@ -27,16 +27,16 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses -import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiListEvent import kotlinx.collections.immutable.toImmutableList @Composable fun WatchAndLoadMyEmojiList(accountViewModel: AccountViewModel) { LoadAddressableNote( - EmojiPackSelectionEvent.createAddress(accountViewModel.userProfile().pubkeyHex), + EmojiListEvent.createAddress(accountViewModel.userProfile().pubkeyHex), ) { emptyNote -> emptyNote?.let { usersEmojiList -> - val collections by observeNoteEventAndMapNotNull(usersEmojiList, accountViewModel) { event: EmojiPackSelectionEvent -> + val collections by observeNoteEventAndMapNotNull(usersEmojiList, accountViewModel) { event: EmojiListEvent -> event.taggedAddresses().toImmutableList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt index 15205698c9..856bddd12c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt @@ -55,7 +55,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.AddDMRelayListDialog import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent @Preview @Composable @@ -88,7 +88,7 @@ fun ObserveRelayListForDMsAndDisplayIfNotFound( @Composable fun ObserveRelayListForDMs( accountViewModel: AccountViewModel, - inner: @Composable (relayListEvent: ChatMessageRelayListEvent?) -> Unit, + inner: @Composable (relayListEvent: DmRelayListEvent?) -> Unit, ) { ObserveRelayListForDMs( pubkey = accountViewModel.account.userProfile().pubkeyHex, @@ -102,13 +102,13 @@ fun ObserveRelayListForDMs( fun ObserveRelayListForDMs( pubkey: HexKey, accountViewModel: AccountViewModel, - inner: @Composable (relayListEvent: ChatMessageRelayListEvent?) -> Unit, + inner: @Composable (relayListEvent: DmRelayListEvent?) -> Unit, ) { LoadAddressableNote( - ChatMessageRelayListEvent.createAddress(pubkey), + DmRelayListEvent.createAddress(pubkey), ) { relayList -> if (relayList != null) { - val relayListEvent by observeNoteEvent<ChatMessageRelayListEvent>(relayList, accountViewModel) + val relayListEvent by observeNoteEvent<DmRelayListEvent>(relayList, accountViewModel) inner(relayListEvent) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteActionSections.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteActionSections.kt index cbb23ef5a4..958acad531 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteActionSections.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NoteActionSections.kt @@ -80,7 +80,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent import kotlinx.coroutines.Dispatchers @@ -222,7 +222,7 @@ fun noteActionSections( // nostr source pre-tagged (`a` for an addressable article, else `e`) plus the author, // and the passage left for the user to type or paste. Prose kinds only, and never a // private rumor (a public highlight would e-tag the unsigned rumor onto relays). - if (!isPrivateRumor && (note.event is TextNoteEvent || note.event is LongTextNoteEvent)) { + if (!isPrivateRumor && (note.event is TextNoteEvent || note.event is LongFormContentEvent)) { add( NoteAction(MaterialSymbols.FormatQuote, stringRes(Res.string.highlight_action)) { val author = note.author?.pubkeyHex @@ -256,7 +256,7 @@ fun noteActionSections( onClick = handlers.onEditPost, ), ) - } else if (note.event is LongTextNoteEvent && state.isLoggedUser) { + } else if (note.event is LongFormContentEvent && state.isLoggedUser) { add( NoteAction(MaterialSymbols.Edit, stringRes(Res.string.edit_article)) { nav.nav { Route.NewLongFormPost(version = note.idHex) } @@ -343,10 +343,10 @@ fun noteActionSections( } else -> { - val noteBookmarkType = if (note.event is LongTextNoteEvent) stringRes(Res.string.article) else stringRes(Res.string.post) + val noteBookmarkType = if (note.event is LongFormContentEvent) stringRes(Res.string.article) else stringRes(Res.string.post) add( NoteAction(MaterialSymbols.BookmarkAdd, stringRes(Res.string.manage_bookmark_label, noteBookmarkType)) { - if (note.event is LongTextNoteEvent) { + if (note.event is LongFormContentEvent) { nav.nav(Route.ArticleBookmarkManagement((note as AddressableNote).address)) } else { nav.nav(Route.PostBookmarkManagement(note.idHex)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 8afb6da337..f6550f6e3a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -109,7 +109,7 @@ import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip40Expiration.expiration -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount @@ -352,7 +352,7 @@ open class CommentPostViewModel : this.externalIdentity = (post.event as? CommentEvent)?.scope() mutedNotifies = emptySet() notifyProvenance = emptyMap() - (post.event as? LnZapEvent)?.let { zap -> + (post.event as? ZapReceiptEvent)?.let { zap -> notifying = listOfNotNull(zapSenderToNotify(zap)) } observeCommunityRules(post) @@ -366,7 +366,7 @@ open class CommentPostViewModel : * by an ephemeral key: skip those — tagging the throwaway key is useless, and * tagging the decrypted sender would publicly expose a private zapper. */ - private fun zapSenderToNotify(zapEvent: LnZapEvent): User? { + private fun zapSenderToNotify(zapEvent: ZapReceiptEvent): User? { val request = zapEvent.zapRequest ?: return null if (request.hasAnonTag()) return null if (request.pubKey == account.signer.pubKey) return null @@ -554,7 +554,7 @@ open class CommentPostViewModel : // Replies to zaps notify the zap sender through a plain p tag (the receipt's // author keys above are the lightning provider). The sender chip always comes // back; a missing p tag in the draft means the user muted their bell. - (replyingTo?.event as? LnZapEvent)?.let { zap -> + (replyingTo?.event as? ZapReceiptEvent)?.let { zap -> zapSenderToNotify(zap)?.let { sender -> notifying = ((notifying ?: emptyList()) + sender).distinct() if (!draftEvent.tags.mapNotNull(PTag::parseKey).contains(sender.pubkeyHex)) { @@ -726,7 +726,7 @@ open class CommentPostViewModel : null } } - } else if (replyingToEvent is LnZapEvent) { + } else if (replyingToEvent is ZapReceiptEvent) { val sender = zapSenderToNotify(replyingToEvent) // The sender's chip stays in the list; a muted bell means // the user doesn't want to ping them, so don't tag them. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/SharedNoteCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/SharedNoteCard.kt index 0a54abc2f0..f0d9265a41 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/SharedNoteCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/SharedNoteCard.kt @@ -63,7 +63,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.GalleryThum import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.UrlImageView import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip36SensitiveContent.ContentWarningTag import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent @@ -278,7 +278,7 @@ private fun secondaryLineFor( return when { event is PictureEvent -> stringRes(Res.string.share_as_qr_kind_picture) event is VideoEvent -> stringRes(Res.string.kind_video) - event is LongTextNoteEvent -> stringRes(Res.string.article) + event is LongFormContentEvent -> stringRes(Res.string.article) hasContentImage -> stringRes(Res.string.share_as_qr_kind_picture) else -> "" } @@ -293,7 +293,7 @@ internal fun secondaryBodyTextFor( // Gated: never surface the note's own title, content or alt text, only the kind label above. if (isGated) return null - if (event is LongTextNoteEvent) { + if (event is LongFormContentEvent) { val title = event.title() if (!title.isNullOrBlank()) return title } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarCollectionRender.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarCollectionRender.kt index e28e4021e7..22b1fa72c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarCollectionRender.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CalendarCollectionRender.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.CalendarCollectionCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent /** * Entry for a NIP-52 calendar collection: decodes the [Note] and renders the shared commons @@ -38,7 +38,7 @@ fun RenderCalendarCollectionEvent( accountViewModel: AccountViewModel, nav: INav, ) { - val event = note.event as? CalendarEvent ?: return + val event = note.event as? CalendarCollectionEvent ?: return CalendarCollectionCard(event) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt index 81a4a54709..bda74a2880 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt @@ -35,7 +35,7 @@ import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip31Alts.alt -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent @Composable fun FileHeaderDisplay( @@ -44,7 +44,7 @@ fun FileHeaderDisplay( contentScale: ContentScale, accountViewModel: AccountViewModel, ) { - val event = (note.event as? FileHeaderEvent) ?: return + val event = (note.event as? FileMetadataEvent) ?: return val fullUrl = event.url() ?: return val mimeType = remember(note) { event.mimeType() } val content = remember(note) { event.toMediaContent(note, fullUrl, mimeType) } @@ -72,7 +72,7 @@ fun FileHeaderDisplay( * video event the kind itself asserts nothing about how to render the payload. A null here means * the file belongs in [FileHeaderAttachmentCard] rather than being pushed into the video player. */ -internal fun FileHeaderEvent.toMediaContent( +internal fun FileMetadataEvent.toMediaContent( note: Note, url: String, mimeType: String?, @@ -129,7 +129,7 @@ internal fun FileHeaderEvent.toMediaContent( /** The link card a kind-1063 header falls back to when [toMediaContent] returns null. */ @Composable internal fun FileHeaderAttachmentCard( - event: FileHeaderEvent, + event: FileMetadataEvent, url: String, mimeType: String?, ) { @@ -145,4 +145,4 @@ internal fun FileHeaderAttachmentCard( } /** The human-facing name of the file: NIP-94 `content` when present, else the `alt` tag. */ -private fun FileHeaderEvent.fileDescription(): String? = content.ifEmpty { null } ?: alt() +private fun FileMetadataEvent.fileDescription(): String? = content.ifEmpty { null } ?: alt() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt index 2f394b2fe8..ac06818f09 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt @@ -66,7 +66,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.FollowSetCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -79,7 +79,7 @@ fun DisplayFollowList( nav: INav, ) { val card = - observeNoteEventAndMap(baseNote, accountViewModel) { event: FollowListEvent? -> + observeNoteEventAndMap(baseNote, accountViewModel) { event: StarterPackEvent? -> if (event == null) { FollowSetCard( name = "", @@ -190,7 +190,7 @@ fun RenderFollowSetThumbPreview() { val accountViewModel = mockAccountViewModel() val followCard = - FollowListEvent( + StarterPackEvent( id = "eca31634fce7c9068b56fa8db9f387da70bdcceb3986a77ca1a9844f3128eb5f", pubKey = "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da", createdAt = 1761736286, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Goal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Goal.kt index a272a55618..0c314f5017 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Goal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Goal.kt @@ -60,7 +60,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent +import com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent import com.vitorpamplona.quartz.utils.TimeUtils import java.math.BigDecimal import kotlin.math.roundToInt @@ -71,14 +71,14 @@ fun RenderGoal( accountViewModel: AccountViewModel, nav: INav, ) { - val noteEvent = note.event as? GoalEvent ?: return + val noteEvent = note.event as? ZapGoalEvent ?: return GoalHeader(noteEvent, note, accountViewModel, nav) } @Composable fun GoalHeader( - noteEvent: GoalEvent, + noteEvent: ZapGoalEvent, note: Note, accountViewModel: AccountViewModel, nav: INav, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt index 7e87980caa..c316416092 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt @@ -65,7 +65,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import kotlinx.collections.immutable.toImmutableList private const val WORDS_PER_MINUTE = 225 @@ -77,7 +77,7 @@ fun RenderLongFormContent( accountViewModel: AccountViewModel, nav: INav, ) { - val noteEvent = note.event as? LongTextNoteEvent ?: return + val noteEvent = note.event as? LongFormContentEvent ?: return LongFormHeader(noteEvent, note, accountViewModel) } @@ -85,7 +85,7 @@ fun RenderLongFormContent( @OptIn(ExperimentalLayoutApi::class) @Composable fun LongFormHeader( - noteEvent: LongTextNoteEvent, + noteEvent: LongFormContentEvent, note: Note, accountViewModel: AccountViewModel, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt index d5c9a604ab..2bee2e52d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt @@ -35,7 +35,7 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent @Composable fun RenderNIP90ContentDiscoveryResponse( @@ -47,7 +47,7 @@ fun RenderNIP90ContentDiscoveryResponse( accountViewModel: AccountViewModel, nav: INav, ) { - val noteEvent = note.event as? NIP90ContentDiscoveryResponseEvent ?: return + val noteEvent = note.event as? DvmContentDiscoveryResponseEvent ?: return val callbackUri = remember(note) { note.toNostrUri() } SensitivityWarning( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt index a2afc2ebec..6b6bfb0303 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent +import com.vitorpamplona.quartz.nip90Dvms.status.DvmStatusEvent @Composable fun RenderNIP90Status( @@ -33,7 +33,7 @@ fun RenderNIP90Status( accountViewModel: AccountViewModel, nav: INav, ) { - val noteEvent = note.event as? NIP90StatusEvent ?: return + val noteEvent = note.event as? DvmStatusEvent ?: return Text(text = noteEvent.content) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt index effd49f5e1..64b4fa7b39 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt @@ -56,7 +56,7 @@ import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.getGradient import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds -import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -68,7 +68,7 @@ fun DisplayPeopleList( accountViewModel: AccountViewModel, nav: INav, ) { - val noteEvent = baseNote.event as? PeopleListEvent ?: return + val noteEvent = baseNote.event as? FollowSetEvent ?: return var members by remember { mutableStateOf<ImmutableList<User>>(persistentListOf()) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt index 1a2310f3fc..743ada7075 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt @@ -53,7 +53,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.ChatroomHeader import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip19Bech32.toNpub @@ -67,7 +67,7 @@ fun RenderPrivateMessage( accountViewModel: AccountViewModel, nav: INav, ) { - val noteEvent = note.event as? PrivateDmEvent ?: return + val noteEvent = note.event as? EncryptedDmEvent ?: return val userRoom by remember(note) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Publication.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Publication.kt index ae654a1f05..fdccdb6b39 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Publication.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Publication.kt @@ -80,8 +80,8 @@ import com.vitorpamplona.quartz.experimental.publications.PublicationContentEven import com.vitorpamplona.quartz.experimental.publications.PublicationIndexEvent import com.vitorpamplona.quartz.experimental.publications.PublicationSectionRef import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import kotlinx.collections.immutable.toImmutableList // A 2:3 portrait cover — a book jacket, which is what NKBIP-01's default `book` type is. The @@ -355,8 +355,8 @@ private fun ObservedSectionRow( // list long-form, wiki and spec events as sections too. is PublicationIndexEvent -> event.titleOrIdentifier() is PublicationContentEvent -> event.titleOrIdentifier() - is LongTextNoteEvent -> event.title() - is WikiNoteEvent -> event.title() + is LongFormContentEvent -> event.title() + is WikiArticleEvent -> event.title() // A bookshelf directory lists whatever it likes, including another directory, so the // row has to name the library kinds too -- otherwise a nested entry falls through to // the section placeholder and reads "Untitled section", which it is not. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicationSection.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicationSection.kt index 73fea0a6e4..8eeaeba528 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicationSection.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicationSection.kt @@ -67,7 +67,7 @@ import com.vitorpamplona.quartz.experimental.publications.PublicationIndexEvent import com.vitorpamplona.quartz.experimental.publications.PublicationSectionRef import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent /** * Renders a kind-30041 NKBIP-01 publication section — a chapter, zettel or episode. @@ -331,12 +331,12 @@ private fun wikilinkResolver(event: PublicationContentEvent): (String) -> String link == null -> null link.eventId != null -> - "nostr:" + NEvent.create(link.eventId!!, link.pubKey, WikiNoteEvent.KIND, link.relay) + "nostr:" + NEvent.create(link.eventId!!, link.pubKey, WikiArticleEvent.KIND, link.relay) link.pubKey != null -> "nostr:" + NAddress.create( - WikiNoteEvent.KIND, + WikiArticleEvent.KIND, link.pubKey!!, PublicationContentEvent.normalizeWikilink(link.target), link.relay, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt index e90e933cbb..ec1f485c88 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt @@ -70,7 +70,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.firstTagValueFor import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import kotlinx.collections.immutable.toImmutableList @@ -158,7 +158,7 @@ fun DisplayDMRelayList( accountViewModel: AccountViewModel, nav: INav, ) { - val noteEvent = baseNote.event as? ChatMessageRelayListEvent ?: return + val noteEvent = baseNote.event as? DmRelayListEvent ?: return val relays = remember(noteEvent) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt index 2bb1f341e6..8067e9db46 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt @@ -96,12 +96,11 @@ import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAssetEvent -import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.SoftwareReleaseEvent -import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.asSoftwareRelease import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.isNip82SoftwareRelease import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.tags.AppIdTag import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag +import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged @@ -209,7 +208,7 @@ fun RenderSoftwareApplication( } /** - * Latest NIP-82 [SoftwareReleaseEvent] version for [app], kept live. + * Latest NIP-82 [ReleaseArtifactSetEvent] version for [app], kept live. * * Releases (kind 30063) are separate events that point back to the app via an * `i` tag rather than an `a` tag, so they are never indexed as replies to the @@ -230,7 +229,7 @@ fun produceLatestReleaseVersion(app: SoftwareApplicationEvent): State<String?> { remember(app.pubKey, app.id) { val filter = Filter( - kinds = listOf(SoftwareReleaseEvent.KIND), + kinds = listOf(ReleaseArtifactSetEvent.KIND), authors = listOf(app.pubKey), tags = mapOf(AppIdTag.TAG_NAME to listOf(app.appId())), ) @@ -243,13 +242,13 @@ fun produceLatestReleaseVersion(app: SoftwareApplicationEvent): State<String?> { return flow.collectAsStateWithLifecycle(initialValue = null) } -fun findLatestNip82Release(app: SoftwareApplicationEvent): SoftwareReleaseEvent? = latestNip82Release(nip82ReleaseNotesFor(app), app) +fun findLatestNip82Release(app: SoftwareApplicationEvent): ReleaseArtifactSetEvent? = latestNip82Release(nip82ReleaseNotesFor(app), app) /** Picks the newest NIP-82 release for [app] out of an already-narrowed [notes] collection. */ private fun latestNip82Release( notes: Collection<Note>, app: SoftwareApplicationEvent, -): SoftwareReleaseEvent? { +): ReleaseArtifactSetEvent? { val prefix = "${app.dTag()}@" return notes .mapNotNull { it.asNip82ReleaseFor(prefix) } @@ -259,25 +258,22 @@ private fun latestNip82Release( /** kind-30063 addressables authored by [app] whose `d` tag is `<app-id>@<version>`. */ private fun nip82ReleaseNotesFor(app: SoftwareApplicationEvent): Set<Note> { val prefix = "${app.dTag()}@" - return LocalCache.addressables.filterIntoSet(SoftwareReleaseEvent.KIND, app.pubKey) { _, addr -> + return LocalCache.addressables.filterIntoSet(ReleaseArtifactSetEvent.KIND, app.pubKey) { _, addr -> val ev = addr.event ?: return@filterIntoSet false ev.isNip82SoftwareRelease() && ev.dTag().startsWith(prefix) } } /** - * Re-reads this note as the NIP-82 release for [prefix] (`<app-id>@`). kind 30063 - * collides with NIP-51 `ReleaseArtifactSetEvent`, which is what `EventFactory` - * builds, so we re-parse matching tag arrays as the NIP-82 form. + * This note as the NIP-82 release for [prefix] (`<app-id>@`). kind 30063 is shared + * with NIP-51 release artifact sets, so only events carrying the NIP-82 tags qualify. */ -private fun Note.asNip82ReleaseFor(prefix: String): SoftwareReleaseEvent? = - when (val ev = event) { - null -> null - is SoftwareReleaseEvent -> ev.takeIf { it.dTag().startsWith(prefix) } - else -> if (ev.isNip82SoftwareRelease() && ev.dTag().startsWith(prefix)) ev.asSoftwareRelease() else null - } +private fun Note.asNip82ReleaseFor(prefix: String): ReleaseArtifactSetEvent? { + val ev = event as? ReleaseArtifactSetEvent ?: return null + return ev.takeIf { it.isNip82SoftwareRelease() && it.dTag().startsWith(prefix) } +} -fun findAllNip82Releases(app: SoftwareApplicationEvent): List<SoftwareReleaseEvent> { +fun findAllNip82Releases(app: SoftwareApplicationEvent): List<ReleaseArtifactSetEvent> { val prefix = "${app.dTag()}@" return nip82ReleaseNotesFor(app) .mapNotNull { it.asNip82ReleaseFor(prefix) } @@ -486,25 +482,17 @@ fun RenderSoftwareRelease( accountViewModel: AccountViewModel, nav: INav, ) { - // kind 30063 is shared between NIP-51 ReleaseArtifactSetEvent and NIP-82 SoftwareReleaseEvent; - // EventFactory always returns the NIP-51 class, so we re-parse the same tag array as the - // NIP-82 form when the tag signature matches. - val rawEvent = note.event ?: return - val event: SoftwareReleaseEvent = - if (rawEvent is SoftwareReleaseEvent) { - rawEvent - } else if (rawEvent.isNip82SoftwareRelease()) { - rawEvent.asSoftwareRelease() - } else { - return - } + // kind 30063 is shared between NIP-51 release artifact sets and NIP-82 software + // releases; only render the NIP-82 form here. + val event = note.event as? ReleaseArtifactSetEvent ?: return + if (!event.isNip82SoftwareRelease()) return RenderSoftwareReleaseBody(event = event, accountViewModel = accountViewModel, nav = nav) } @Composable fun RenderSoftwareReleaseBody( - event: SoftwareReleaseEvent, + event: ReleaseArtifactSetEvent, accountViewModel: AccountViewModel, nav: INav, showAppId: Boolean = true, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt index b9bdd996a6..908ff3f919 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt @@ -60,7 +60,7 @@ import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip14Subject.subject import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent enum class ReplyRenderType { FULL, @@ -129,7 +129,7 @@ fun RenderTextEvent( // Zap receipts are signed by the recipient's lightning provider; // label the reply with the zap sender instead of the service key. val zapSender = - if (parentNote.event is LnZapEvent) { + if (parentNote.event is ZapReceiptEvent) { observeZapSender(parentNote, accountViewModel).value } else { null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt index 22d717f56d..be61fdda0a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt @@ -50,7 +50,7 @@ import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent @Composable fun RenderWikiContent( @@ -58,14 +58,14 @@ fun RenderWikiContent( accountViewModel: AccountViewModel, nav: INav, ) { - val noteEvent = note.event as? WikiNoteEvent ?: return + val noteEvent = note.event as? WikiArticleEvent ?: return WikiNoteHeader(noteEvent, note, accountViewModel, nav) } @Composable private fun WikiNoteHeader( - noteEvent: WikiNoteEvent, + noteEvent: WikiArticleEvent, note: Note, accountViewModel: AccountViewModel, nav: INav, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/WikiCollaboration.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/WikiCollaboration.kt index 4338782fd5..521e0e02db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/WikiCollaboration.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/WikiCollaboration.kt @@ -55,9 +55,9 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip54Wiki.WikiMergeAcceptanceEvent import com.vitorpamplona.quartz.nip54Wiki.WikiMergeRequestEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip54Wiki.WikiRedirectEvent /** @@ -215,7 +215,7 @@ private fun WikiArticleRow( ) { LoadAddressableNote(address) { articleNote -> if (articleNote != null) { - val article by observeNoteEvent<WikiNoteEvent>(articleNote, accountViewModel) + val article by observeNoteEvent<WikiArticleEvent>(articleNote, accountViewModel) WikiRow( // The coordinate's own identifier is a readable slug, so it stands in until the @@ -240,7 +240,7 @@ private fun WikiVersionRow( ) { LoadNote(eventId) { versionNote -> if (versionNote != null) { - val article by observeNoteEvent<WikiNoteEvent>(versionNote, accountViewModel) + val article by observeNoteEvent<WikiArticleEvent>(versionNote, accountViewModel) WikiRow( title = article?.title() ?: eventId.take(8), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt index 94c76b141e..bc6d1bb5f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt @@ -64,7 +64,7 @@ import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -104,7 +104,7 @@ fun RenderLnZap( accountViewModel: AccountViewModel, nav: INav, ) { - val zapEvent = note.event as? LnZapEvent ?: return + val zapEvent = note.event as? ZapReceiptEvent ?: return val card by parseAuthorCommentAndAmount(note, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/BookmarkLists.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/BookmarkLists.kt index 157bf7f0de..2a8a1ce57b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/BookmarkLists.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/BookmarkLists.kt @@ -38,7 +38,7 @@ import com.vitorpamplona.quartz.nip51Lists.articleCurationSet.ArticleCurationSet import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent import com.vitorpamplona.quartz.nip51Lists.pictureCurationSet.PictureCurationSetEvent /** NIP-51 kind 10003: the user's bookmarks — notes, articles, hashtags and links. */ @@ -108,7 +108,7 @@ fun RenderLabeledBookmarkList( accountViewModel: AccountViewModel, nav: INav, ) { - val noteEvent = baseNote.event as? LabeledBookmarkListEvent ?: return + val noteEvent = baseNote.event as? BookmarkSetEvent ?: return val public = remember(noteEvent) { noteEvent.publicBookmarks() } val private by loadPrivateItems(noteEvent, accountViewModel) { noteEvent.privateBookmarks(it) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/TagLists.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/TagLists.kt index 4294e5386d..b2186cbd21 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/TagLists.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/TagLists.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent /** NIP-51 kind 10015: the hashtags this user follows. */ @@ -47,10 +47,10 @@ fun RenderHashtagList( accountViewModel: AccountViewModel, nav: INav, ) { - val noteEvent = baseNote.event as? HashtagListEvent ?: return + val noteEvent = baseNote.event as? InterestListEvent ?: return val public = remember(noteEvent) { noteEvent.publicHashtags() } - // HashtagListEvent exposes no private accessor of its own, but it is a PrivateTagArrayEvent + // InterestListEvent exposes no private accessor of its own, but it is a PrivateTagArrayEvent // like the rest, so its private half is read the same way InterestSetEvent reads its own. val private by loadPrivateItems(noteEvent, accountViewModel) { signer -> noteEvent.privateTags(signer)?.mapNotNull(HashtagTag::parse) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt index 5a3eaf6059..8d0618d42d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt @@ -43,9 +43,9 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.utils.Log @@ -594,9 +594,9 @@ class PeopleListName( NoteBackedName { override fun name(): String { val noteEvent = note.event - return if (noteEvent is PeopleListEvent) { + return if (noteEvent is FollowSetEvent) { noteEvent.titleOrName() ?: note.dTag() - } else if (noteEvent is FollowListEvent) { + } else if (noteEvent is StarterPackEvent) { noteEvent.title() ?: note.dTag() } else { note.dTag() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 04244170d0..6cd968367e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -172,7 +172,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip03Timestamp.EmptyOtsResolverBuilder -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip05DnsIdentifiers.EmptyNip05Client import com.vitorpamplona.quartz.nip05DnsIdentifiers.INip05Client import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Client @@ -202,16 +202,16 @@ import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent import com.vitorpamplona.quartz.nip56Reports.ReportType -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlForm import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip60Cashu.token.CashuToken -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip92IMeta.imeta import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import com.vitorpamplona.quartz.podcasts.PodcastBoostagram @@ -725,7 +725,7 @@ class AccountViewModel( val isHiddenAuthor = note.author?.let { account.isHidden(it) } == true val noteEvent = note.event - val isDecryptedPostHidden = if (noteEvent is PrivateDmEvent) account.isDecryptedContentHidden(noteEvent) else false + val isDecryptedPostHidden = if (noteEvent is EncryptedDmEvent) account.isDecryptedContentHidden(noteEvent) else false return if (isPostHidden || isDecryptedPostHidden) { // Spam + Blocked Users + Hidden Words + Sensitive Content @@ -924,14 +924,14 @@ class AccountViewModel( it.request.event ?.content ?.ifBlank { null }, - showAmountInteger((it.response.event as? LnZapEvent)?.amount), + showAmountInteger((it.response.event as? ZapReceiptEvent)?.amount), it.response, ) }.toMutableMap() val results = mapNotNullAsync( - zaps.filter { (it.request.event as? LnZapRequestEvent)?.isPrivateZap() == true }, + zaps.filter { (it.request.event as? ZapRequestEvent)?.isPrivateZap() == true }, ) { next -> val info = innerDecryptAmountMessage(next.request, next.response) if (info != null) { @@ -950,14 +950,14 @@ class AccountViewModel( fun cachedDecryptAmountMessageInGroup(zapNotes: List<CombinedZap>): ImmutableList<ZapAmountCommentNotification> = zapNotes .map { - val request = it.request.event as? LnZapRequestEvent + val request = it.request.event as? ZapRequestEvent if (request?.isPrivateZap() == true) { val cachedPrivateRequest = account.privateZapsDecryptionCache.cachedPrivateZap(request) if (cachedPrivateRequest != null) { ZapAmountCommentNotification( LocalCache.getUserIfExists(cachedPrivateRequest.pubKey) ?: it.request.author, cachedPrivateRequest.content.ifBlank { null }, - showAmountInteger((it.response.event as? LnZapEvent)?.amount), + showAmountInteger((it.response.event as? ZapReceiptEvent)?.amount), it.response, ) } else { @@ -966,7 +966,7 @@ class AccountViewModel( it.request.event ?.content ?.ifBlank { null }, - showAmountInteger((it.response.event as? LnZapEvent)?.amount), + showAmountInteger((it.response.event as? ZapReceiptEvent)?.amount), it.response, ) } @@ -976,7 +976,7 @@ class AccountViewModel( it.request.event ?.content ?.ifBlank { null }, - showAmountInteger((it.response.event as? LnZapEvent)?.amount), + showAmountInteger((it.response.event as? ZapReceiptEvent)?.amount), it.response, ) } @@ -987,14 +987,14 @@ class AccountViewModel( return myList .map { - val request = it.first.event as? LnZapRequestEvent + val request = it.first.event as? ZapRequestEvent if (request?.isPrivateZap() == true) { val cachedPrivateRequest = account.privateZapsDecryptionCache.cachedPrivateZap(request) if (cachedPrivateRequest != null) { ZapAmountCommentNotification( LocalCache.getUserIfExists(cachedPrivateRequest.pubKey) ?: it.first.author, cachedPrivateRequest.content.ifBlank { null }, - showAmountInteger((it.second?.event as? LnZapEvent)?.amount), + showAmountInteger((it.second?.event as? ZapReceiptEvent)?.amount), it.second, ) } else { @@ -1003,7 +1003,7 @@ class AccountViewModel( it.first.event ?.content ?.ifBlank { null }, - showAmountInteger((it.second?.event as? LnZapEvent)?.amount), + showAmountInteger((it.second?.event as? ZapReceiptEvent)?.amount), it.second, ) } @@ -1013,7 +1013,7 @@ class AccountViewModel( it.first.event ?.content ?.ifBlank { null }, - showAmountInteger((it.second?.event as? LnZapEvent)?.amount), + showAmountInteger((it.second?.event as? ZapReceiptEvent)?.amount), it.second, ) } @@ -1036,7 +1036,7 @@ class AccountViewModel( it.first.event ?.content ?.ifBlank { null }, - showAmountInteger((it.second?.event as? LnZapEvent)?.amount), + showAmountInteger((it.second?.event as? ZapReceiptEvent)?.amount), it.second, ) }.toMutableMap() @@ -1073,7 +1073,7 @@ class AccountViewModel( } suspend fun innerDecryptAmountMessage(zapNote: Note): ZapAmountCommentNotification? { - val zapEvent = zapNote.event as? LnZapEvent ?: return null + val zapEvent = zapNote.event as? ZapReceiptEvent ?: return null val zapRequest = zapEvent.zapRequest ?: return null return innerDecryptAmountMessage(zapRequest, zapEvent)?.copy(zapNote = zapNote) @@ -1083,14 +1083,14 @@ class AccountViewModel( zapRequest: Note, zapEvent: Note, ): ZapAmountCommentNotification? { - val zapEvent = zapEvent.event as? LnZapEvent ?: return null - val zapRequest = zapRequest.event as? LnZapRequestEvent ?: return null + val zapEvent = zapEvent.event as? ZapReceiptEvent ?: return null + val zapRequest = zapRequest.event as? ZapRequestEvent ?: return null return innerDecryptAmountMessage(zapRequest, zapEvent) } suspend fun innerDecryptAmountMessage( - zapRequestEvent: LnZapRequestEvent, - zapEvent: LnZapEvent, + zapRequestEvent: ZapRequestEvent, + zapEvent: ZapReceiptEvent, ): ZapAmountCommentNotification? { val amount = showAmountInteger(zapEvent.amount) return if (zapRequestEvent.isPrivateZap()) { @@ -1148,7 +1148,7 @@ class AccountViewModel( onError: (String, String, User?) -> Unit, onProgress: (percent: Float) -> Unit, onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit, - zapType: LnZapEvent.ZapType? = null, + zapType: ZapReceiptEvent.ZapType? = null, ) = launchSigner { // A podcast note (episode or show) can carry a Podcasting-2.0 value-for-value block. When it // does, "zapping" it means paying that split — lnaddress recipients go out as real zaps (with @@ -1177,8 +1177,8 @@ class AccountViewModel( // comment stay encrypted. NONZAP is kept: paying without a zap // request produces no receipt at all, which is even more private. val effectiveType = - if (note.isPrivateRumor() && requestedType != LnZapEvent.ZapType.NONZAP) { - LnZapEvent.ZapType.PRIVATE + if (note.isPrivateRumor() && requestedType != ZapReceiptEvent.ZapType.NONZAP) { + ZapReceiptEvent.ZapType.PRIVATE } else { requestedType } @@ -1299,7 +1299,7 @@ class AccountViewModel( zappedNote = zappedNote, context = context, asZap = !streaming, - zapType = LnZapEvent.ZapType.PUBLIC, + zapType = ZapReceiptEvent.ZapType.PUBLIC, okHttpClient = httpClientBuilder::okHttpClientForMoney, onError = { title, message -> if (!streaming) toastManager.toast(title, message) @@ -1463,7 +1463,7 @@ class AccountViewModel( fun follows(user: User): Note = LocalCache.getOrCreateAddressableNote(ContactListEvent.createAddress(user.pubkeyHex)) - fun hashtagFollows(user: User): Note = LocalCache.getOrCreateAddressableNote(HashtagListEvent.createAddress(user.pubkeyHex)) + fun hashtagFollows(user: User): Note = LocalCache.getOrCreateAddressableNote(InterestListEvent.createAddress(user.pubkeyHex)) fun bookmarks(user: User): Note = LocalCache.getOrCreateAddressableNote(BookmarkListEvent.createBookmarkAddress(user.pubkeyHex)) @@ -2122,7 +2122,7 @@ class AccountViewModel( fun updateZapAmounts( amountSet: List<Long>, - selectedZapType: LnZapEvent.ZapType, + selectedZapType: ZapReceiptEvent.ZapType, nip47Update: Nip47WalletConnect.Nip47URINorm?, ) = launchSigner { account.updateZapAmounts(amountSet, selectedZapType, nip47Update) } @@ -2848,7 +2848,7 @@ class AccountViewModel( } } - private suspend fun unwrapSeal(event: SealedRumorEvent): Note? { + private suspend fun unwrapSeal(event: SealEvent): Note? { val cacheInnerEventId = event.innerEventId return if (cacheInnerEventId != null) { val existingNoteEvent = LocalCache.getNoteIfExists(cacheInnerEventId)?.event @@ -2882,7 +2882,7 @@ class AccountViewModel( private suspend fun unwrapIfNeeded(event: Event): Note? = when (event) { is GiftWrapEvent -> unwrapGiftWrap(event) - is SealedRumorEvent -> unwrapSeal(event) + is SealEvent -> unwrapSeal(event) else -> LocalCache.getNoteIfExists(event.id) } @@ -2936,7 +2936,7 @@ class AccountViewModel( LocalCache.notes.maxOrNullOf( filter = { _, note -> val noteEvent = note.event - noteEvent is NIP90ContentDiscoveryResponseEvent && + noteEvent is DvmContentDiscoveryResponseEvent && noteEvent.pubKey == pubkeyHex && noteEvent.isTaggedUser(account.signer.pubKey) && noteEvent.createdAt > fifteenMinsAgo @@ -3005,13 +3005,13 @@ class AccountViewModel( onError: (String, String) -> Unit, onProgress: (percent: Float) -> Unit, context: Context, - zapType: LnZapEvent.ZapType? = null, + zapType: ZapReceiptEvent.ZapType? = null, ) { viewModelScope.launch(Dispatchers.IO) { try { val effectiveZapType = zapType ?: defaultZapType() val zapRequest = - if (effectiveZapType != LnZapEvent.ZapType.NONZAP) { + if (effectiveZapType != ZapReceiptEvent.ZapType.NONZAP) { // NIP-57 Appendix F: include amount + lnurl so the receipt can be validated. val splitLnurl = LnurlForm.toUrl(lnAddress)?.let(LnurlForm::urlToBech32) account.zaps.createZapRequestFor( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index ab256b5804..794e1b3027 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -42,10 +42,10 @@ import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.PrivateZapCache -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallAnswerEvent @@ -112,9 +112,9 @@ class EventProcessor( is GiftWrapEvent -> giftWrapHandler.add(event, eventNote, publicNote) - is SealedRumorEvent -> sealHandler.add(event, eventNote, publicNote) + is SealEvent -> sealHandler.add(event, eventNote, publicNote) - is LnZapRequestEvent -> zapRequest.add(event, eventNote, publicNote) + is ZapRequestEvent -> zapRequest.add(event, eventNote, publicNote) } } @@ -153,9 +153,9 @@ class EventProcessor( is ChatroomKeyable -> chatHandler.delete(event, note) is DraftWrapEvent -> draftHandler.delete(event, note) is GiftWrapEvent -> giftWrapHandler.delete(event, note) - is SealedRumorEvent -> sealHandler.delete(event, note) - is LnZapRequestEvent -> zapRequest.delete(event, note) - is LnZapEvent -> zapEvent.delete(event, note) + is SealEvent -> sealHandler.delete(event, note) + is ZapRequestEvent -> zapRequest.delete(event, note) + is ZapReceiptEvent -> zapEvent.delete(event, note) } } @@ -457,9 +457,9 @@ class SealedRumorEventHandler( private val account: Account, private val cache: LocalCache, private val eventProcessor: EventProcessor, -) : EventHandler<SealedRumorEvent> { +) : EventHandler<SealEvent> { override suspend fun add( - event: SealedRumorEvent, + event: SealEvent, eventNote: Note, publicNote: Note, ) { @@ -477,7 +477,7 @@ class SealedRumorEventHandler( } override suspend fun delete( - event: SealedRumorEvent, + event: SealEvent, eventNote: Note, ) { event.innerEventId?.let { rumorId -> @@ -489,7 +489,7 @@ class SealedRumorEventHandler( } private suspend fun processNewSealedRumor( - event: SealedRumorEvent, + event: SealEvent, eventNote: Note, publicNote: Note, ) { @@ -538,9 +538,9 @@ class SealedRumorEventHandler( class LnZapRequestEventHandler( val decryptionCache: PrivateZapCache, -) : EventHandler<LnZapRequestEvent> { +) : EventHandler<ZapRequestEvent> { override suspend fun add( - event: LnZapRequestEvent, + event: ZapRequestEvent, eventNote: Note, publicNote: Note, ) { @@ -550,7 +550,7 @@ class LnZapRequestEventHandler( } override suspend fun delete( - event: LnZapRequestEvent, + event: ZapRequestEvent, eventNote: Note, ) { if (event.isPrivateZap()) { @@ -561,9 +561,9 @@ class LnZapRequestEventHandler( class LnZapEventHandler( val decryptionCache: PrivateZapCache, -) : EventHandler<LnZapEvent> { +) : EventHandler<ZapReceiptEvent> { override suspend fun delete( - event: LnZapEvent, + event: ZapReceiptEvent, eventNote: Note, ) { event.zapRequest?.let { req -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesFeedLoaded.kt index d2a3d6e0d5..13ab488bfd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesFeedLoaded.kt @@ -37,7 +37,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.ChannelCardCompose -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent @Composable fun ArticlesFeedLoaded( @@ -61,7 +61,7 @@ fun ArticlesFeedLoaded( ChannelCardCompose( baseNote = item, modifier = Modifier.fillMaxWidth(), - forceEventKind = LongTextNoteEvent.KIND, + forceEventKind = LongFormContentEvent.KIND, accountViewModel = accountViewModel, nav = nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesTopBar.kt index 4163e6e015..922e4f8445 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/ArticlesTopBar.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar import com.vitorpamplona.amethyst.ui.screen.FeedDefinition import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent @Composable fun ArticlesTopBar( @@ -49,7 +49,7 @@ fun ArticlesTopBar( // The feed's own kind window, plus whatever the list spinner narrowed it to — a hashtag or a // geohash says itself as a token; a follow set does not, and seeds nothing (see asSearchQuery). val me = accountViewModel.userProfile().pubkeyHex - val seed = remember(list, me) { SearchSeed.merge(SearchSeed.ofKinds(LongTextNoteEvent.KIND), list.asSearchQuery(me)) } + val seed = remember(list, me) { SearchSeed.merge(SearchSeed.ofKinds(LongFormContentEvent.KIND), list.asSearchQuery(me)) } UserDrawerSearchTopBar(accountViewModel, nav, seed) { ArticlesTopNavFilterBar( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/dal/ArticlesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/dal/ArticlesFeedFilter.kt index 5ec4bd4976..7bea4bb080 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/dal/ArticlesFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/dal/ArticlesFeedFilter.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent class ArticlesFeedFilter( val account: Account, @@ -50,9 +50,9 @@ class ArticlesFeedFilter( override fun feed(): List<Note> { val params = buildFilterParams(account) val notes = - LocalCache.addressables.filterIntoSet(LongTextNoteEvent.KIND) { _, it -> + LocalCache.addressables.filterIntoSet(LongFormContentEvent.KIND) { _, it -> val noteEvent = it.event - noteEvent is LongTextNoteEvent && params.match(noteEvent, it.relays) + noteEvent is LongFormContentEvent && params.match(noteEvent, it.relays) } return sort(notes) } @@ -70,7 +70,7 @@ class ArticlesFeedFilter( return collection.filterTo(HashSet()) { val noteEvent = it.event - noteEvent is LongTextNoteEvent && params.match(noteEvent, it.relays) + noteEvent is LongFormContentEvent && params.match(noteEvent, it.relays) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt index 55473cce27..6587568294 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt @@ -92,9 +92,9 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.diff.ContentChange import com.vitorpamplona.quartz.nip01Core.diff.ListDiff import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListDiff -import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent +import com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListEvent import com.vitorpamplona.quartz.nip28PublicChat.list.channels import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent @@ -103,10 +103,10 @@ import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFee import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListDiff import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListDiff -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayListDiff import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip60Cashu.wallet.CashuWalletDiff @@ -137,7 +137,7 @@ internal fun LazyListScope.listDiffItems( is CommunityListDiff -> communityItems(diff, conflict, accountViewModel, nav) is EphemeralChatListDiff -> ephemeralRoomItems(diff, conflict, nav) is FavoriteAlgoFeedsListDiff -> favoriteFeedItems(diff, conflict, accountViewModel, nav) - is HashtagListDiff -> topicItems(diff.hashtags, (conflict.saved as? HashtagListEvent)?.publicHashtags().orEmpty(), diff.privateItems, isPlace = false, nav) + is HashtagListDiff -> topicItems(diff.hashtags, (conflict.saved as? InterestListEvent)?.publicHashtags().orEmpty(), diff.privateItems, isPlace = false, nav) is GeohashListDiff -> topicItems(diff.geohashes, (conflict.saved as? GeohashListEvent)?.publicGeohashes().orEmpty(), diff.privateItems, isPlace = true, nav) is TrustProviderListDiff -> trustProviderItems(diff, conflict, accountViewModel, nav) is PaymentTargetsDiff -> paymentTargetItems(diff, conflict) @@ -236,11 +236,11 @@ private fun framingOf(type: BackupEventType): RelayFraming = private fun savedRelaysOf(event: Event): List<NormalizedRelayUrl> = when (event) { - is ChatMessageRelayListEvent -> event.relays() + is DmRelayListEvent -> event.relays() is KeyPackageRelayListEvent -> event.relays() is SearchRelayListEvent -> event.publicRelays() is IndexerRelayListEvent -> event.publicRelays() - is RelayFeedsListEvent -> event.publicRelays() + is FavoriteRelayListEvent -> event.publicRelays() is BlockedRelayListEvent -> event.publicRelays() is TrustedRelayListEvent -> event.publicRelays() is PrivateOutboxRelayListEvent -> event.publicRelays() @@ -451,7 +451,7 @@ private fun LazyListScope.publicChatItems( accountViewModel: AccountViewModel, nav: INav, ) { - val saved = (conflict.saved as? ChannelListEvent)?.tags?.channels().orEmpty() + val saved = (conflict.saved as? PublicChatListEvent)?.tags?.channels().orEmpty() spaceGrid("chat-", fates(saved, diff.channels) { it.eventId }, { it.eventId }) { tag, fate, modifier -> LoadPublicChatChannel(tag.eventId) { channel -> // Subscribes to the channel's metadata on relays and recomposes when it arrives. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt index b2a31af51a..4c31575212 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupItemOptions.kt @@ -75,7 +75,7 @@ import com.vitorpamplona.amethyst.ui.note.types.EditState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.report.ReportNoteDialog import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -281,7 +281,7 @@ fun BookmarkGroupItemOptionsMenu( wantsToEditPost.value = true } } - } else if (note.event is LongTextNoteEvent && state.isLoggedUser) { + } else if (note.event is LongFormContentEvent && state.isLoggedUser) { M3ActionRow(icon = MaterialSymbols.Edit, text = stringRes(Res.string.edit_article)) { nav.nav { Route.NewLongFormPost(version = note.idHex) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsTopBar.kt index 996453f5e7..c5c54cff10 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsTopBar.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar import com.vitorpamplona.amethyst.ui.screen.FeedDefinition import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent @Composable fun CalendarCollectionsTopBar( @@ -49,7 +49,7 @@ fun CalendarCollectionsTopBar( // The feed's own kind window, plus whatever the list spinner narrowed it to — a hashtag // or a geohash says itself as a token; a follow set does not, and seeds nothing. val me = accountViewModel.userProfile().pubkeyHex - val seed = remember(list, me) { SearchSeed.merge(SearchSeed.ofKinds(CalendarEvent.KIND), list.asSearchQuery(me)) } + val seed = remember(list, me) { SearchSeed.merge(SearchSeed.ofKinds(CalendarCollectionEvent.KIND), list.asSearchQuery(me)) } UserDrawerSearchTopBar(accountViewModel, nav, seed) { CalendarCollectionsTopNavFilterBar( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt index 644d10c45e..ea7cc7c295 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt @@ -76,7 +76,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.UserCardHeader import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import com.vitorpamplona.quartz.utils.TimeUtils @Composable @@ -153,7 +153,7 @@ fun CalendarCollectionCard( accountViewModel: AccountViewModel, nav: INav, ) { - val event = note.event as? CalendarEvent ?: return + val event = note.event as? CalendarCollectionEvent ?: return val title = remember(note.idHex) { event.title() } val description = remember(note.idHex) { event.content.take(180) } val count = remember(note.idHex) { event.calendarEventAddresses().size } @@ -241,7 +241,7 @@ fun CalendarCollectionCard( * arrived from relays yet or aren't appointments. Returns a list compatible with * [IcsExport.calendarToIcs]. */ -private fun collectMembers(calendar: CalendarEvent): List<Pair<Address, Any>> = +private fun collectMembers(calendar: CalendarCollectionEvent): List<Pair<Address, Any>> = calendar .calendarEventAddresses() .mapNotNull { addr -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsViewModel.kt index dc74d818fa..5b41ea50f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsViewModel.kt @@ -39,7 +39,7 @@ import com.vitorpamplona.amethyst.commons.model.nip52Calendar.groupByDayKeyExpan import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -162,13 +162,13 @@ class CalendarsViewModel : ViewModel() { * changes about once a week. */ @OptIn(ExperimentalCoroutinesApi::class) - val ownCalendars: StateFlow<List<CalendarEvent>> = + val ownCalendars: StateFlow<List<CalendarCollectionEvent>> = inputs .filterNotNull() .flatMapLatest { (myPubKey, _) -> LocalCache - .observeEvents<CalendarEvent>( - Filter(kinds = listOf(CalendarEvent.KIND), authors = listOf(myPubKey)), + .observeEvents<CalendarCollectionEvent>( + Filter(kinds = listOf(CalendarCollectionEvent.KIND), authors = listOf(myPubKey)), ).map { calendars -> calendars.sortedBy { it.title()?.lowercase() ?: "" } } } // The seed scan inside observeEvents walks the whole notes cache (LocalCache.filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt index 62fc41a72a..f389c7a9c1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag import com.vitorpamplona.quartz.nip01Core.tags.aTag.aTags import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -62,7 +62,7 @@ class NewCalendarCollectionViewModel : ViewModel() { private var dTag: String? = null /** The full original event in edit mode; needed to publish a NIP-09 deletion. */ - private var loadedEvent: com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent? = null + private var loadedEvent: com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent? = null val selectedAddresses = mutableStateListOf<Address>() val availableAppointments = mutableStateOf<List<OwnedAppointmentSummary>>(emptyList()) @@ -81,9 +81,9 @@ class NewCalendarCollectionViewModel : ViewModel() { dTag = editDTag editDTag?.let { existingDTag -> - val existingAddress = Address(CalendarEvent.KIND, account.userProfile().pubkeyHex, existingDTag) + val existingAddress = Address(CalendarCollectionEvent.KIND, account.userProfile().pubkeyHex, existingDTag) val existingNote = LocalCache.addressables.get(existingAddress) - (existingNote?.event as? CalendarEvent)?.let { existing -> + (existingNote?.event as? CalendarCollectionEvent)?.let { existing -> loadedEvent = existing title.value = existing.title().orEmpty() description.value = existing.content @@ -140,7 +140,7 @@ class NewCalendarCollectionViewModel : ViewModel() { account.signAndComputeBroadcast( if (effectiveDTag != null) { - CalendarEvent.build( + CalendarCollectionEvent.build( title = parsedTitle, content = parsedDescription, dTag = effectiveDTag, @@ -148,7 +148,7 @@ class NewCalendarCollectionViewModel : ViewModel() { if (selected.isNotEmpty()) aTags(selected.map { ATag(it) }) } } else { - CalendarEvent.build( + CalendarCollectionEvent.build( title = parsedTitle, content = parsedDescription, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/dal/CalendarCollectionsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/dal/CalendarCollectionsFeedFilter.kt index c26935be56..7396b10d5a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/dal/CalendarCollectionsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/dal/CalendarCollectionsFeedFilter.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent class CalendarCollectionsFeedFilter( val account: Account, @@ -49,7 +49,7 @@ class CalendarCollectionsFeedFilter( val notes = LocalCache.addressables.filterIntoSet { _, it -> val e = it.event - e is CalendarEvent && params.match(e, it.relays) + e is CalendarCollectionEvent && params.match(e, it.relays) } return sort(notes) } @@ -66,7 +66,7 @@ class CalendarCollectionsFeedFilter( val params = buildFilterParams(account) return collection.filterTo(HashSet()) { val e = it.event - e is CalendarEvent && params.match(e, it.relays) + e is CalendarCollectionEvent && params.match(e, it.relays) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.kt index 8f9deda35d..6d878afb92 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/AddToCalendarSheet.kt @@ -53,7 +53,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag import com.vitorpamplona.quartz.nip01Core.tags.aTag.aTags -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent /** * Bottom sheet that lists the current user's own kind-31924 calendars with a checkbox per @@ -74,7 +74,7 @@ fun AddToCalendarSheet( val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val myPubKey = accountViewModel.userProfile().pubkeyHex - val ownCalendars by produceState<List<CalendarEvent>>(initialValue = ownCalendars(myPubKey), myPubKey) { + val ownCalendars by produceState<List<CalendarCollectionEvent>>(initialValue = ownCalendars(myPubKey), myPubKey) { LocalCache.live.newEventBundles.collect { value = ownCalendars(myPubKey) } @@ -152,17 +152,17 @@ private fun CalendarPickerRow( } } -private fun ownCalendars(myPubKey: String): List<CalendarEvent> = +private fun ownCalendars(myPubKey: String): List<CalendarCollectionEvent> = LocalCache.addressables .filterIntoSet { _, note -> val e = note.event - e is CalendarEvent && e.pubKey == myPubKey - }.mapNotNull { it.event as? CalendarEvent } + e is CalendarCollectionEvent && e.pubKey == myPubKey + }.mapNotNull { it.event as? CalendarCollectionEvent } .sortedBy { it.title()?.lowercase() ?: "" } private fun toggleMembership( accountViewModel: AccountViewModel, - calendar: CalendarEvent, + calendar: CalendarCollectionEvent, targetAddress: Address, isCurrentlyMember: Boolean, ) { @@ -177,7 +177,7 @@ private fun toggleMembership( } accountViewModel.launchSigner { accountViewModel.account.signAndComputeBroadcast( - CalendarEvent.build( + CalendarCollectionEvent.build( title = calendar.title().orEmpty(), content = calendar.content, dTag = calendar.dTag(), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt index beece330fa..3072750bd7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/detail/CalendarEventDetailScreen.kt @@ -116,7 +116,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.tags.RSVPStatusTag import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import com.vitorpamplona.quartz.nip52Calendar.rsvp.CalendarRSVPEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.Dispatchers @@ -287,7 +287,7 @@ fun CalendarEventDetailScreen( targetAddress = targetAddress, ) - is CalendarEvent -> + is CalendarCollectionEvent -> CollectionBody( note = targetNote, accountViewModel = accountViewModel, @@ -432,7 +432,7 @@ private fun CollectionBody( nav: INav, targetAddress: Address, ) { - val event = note.event as? CalendarEvent ?: return + val event = note.event as? CalendarCollectionEvent ?: return val title = remember(note.idHex) { event.title() } val memberAddresses = remember(note.idHex) { event.calendarEventAddresses() } @@ -736,7 +736,7 @@ private fun InCalendarsSection( .clickable { nav.nav( Route.CalendarEventDetail( - kind = CalendarEvent.KIND, + kind = CalendarCollectionEvent.KIND, pubKeyHex = calendar.pubKey, dTag = calendar.dTag(), ), @@ -889,11 +889,11 @@ private fun rememberRsvpsFor(targetAddress: Address): State<List<CalendarRSVPEve } @Composable -private fun rememberCalendarsContaining(targetAddress: Address): State<List<CalendarEvent>> { +private fun rememberCalendarsContaining(targetAddress: Address): State<List<CalendarCollectionEvent>> { val calendars = remember(targetAddress) { LocalCache - .observeEvents<CalendarEvent>(Filter(kinds = listOf(CalendarEvent.KIND))) + .observeEvents<CalendarCollectionEvent>(Filter(kinds = listOf(CalendarCollectionEvent.KIND))) .map { all -> all .filter { it.calendarEventAddresses().contains(targetAddress) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt index ccb19c4c2d..9ecb9c8776 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt @@ -87,7 +87,7 @@ import com.vitorpamplona.quartz.buzz.stream.SystemMessageEvent import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChatEditEvent import com.vitorpamplona.quartz.marmot.foundation.appEvents.MarmotAppEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip10Notes.BaseNoteEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent @@ -97,7 +97,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent import com.vitorpamplona.quartz.nip53LiveActivities.raid.LiveActivitiesRaidEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip57Zaps.splits.hasZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount @@ -156,7 +156,7 @@ fun ChatroomMessageCompose( } val event = baseNote.event - if (event is LnZapEvent) { + if (event is ZapReceiptEvent) { RenderChatZap(baseNote, accountViewModel, nav) } else if (event is LiveActivitiesRaidEvent) { RenderChatRaid(baseNote, accountViewModel, nav) @@ -248,10 +248,10 @@ fun NormalChatNote( when { // Own messages: normally no name; in a multi-identity chat, show it (unless a DM, // which never draws the user's own author info). - isLoggedInUser -> showSelfAuthorName && noteEvent !is PrivateDmEvent + isLoggedInUser -> showSelfAuthorName && noteEvent !is EncryptedDmEvent // never shows the user's pictures - noteEvent is PrivateDmEvent -> false + noteEvent is EncryptedDmEvent -> false // one-on-one, never shows it. // only shows in a group chat. @@ -588,7 +588,7 @@ private fun RenderReply( is ChatMessageEvent, is ChatMessageEncryptedFileHeaderEvent -> { { LoadingReplyNote(DmReplyProtocol.NIP17, accountViewModel) } } - is PrivateDmEvent -> { + is EncryptedDmEvent -> { { LoadingReplyNote(DmReplyProtocol.NIP04, accountViewModel) } } else -> null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt index 7f0627d365..3f0c1a3f0d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt @@ -51,7 +51,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.ToggleableTimeAgoText import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip13Pow.strongPoWOrNull import com.vitorpamplona.quartz.nip40Expiration.expiration @@ -84,7 +84,7 @@ fun chatFooterHasMeta( ): Boolean { val event = note.event ?: return false val geo = event.geoHashOrScope() - return event is PrivateDmEvent || + return event is EncryptedDmEvent || event.expiration() != null || (geo != null && geo != suppressGeohash) || event.strongPoWOrNull() != null || @@ -132,7 +132,7 @@ fun ChatMessageFooter( } if (showTime) { - val hasGlyph = note.isPinnedInRelayGroup() || event is PrivateDmEvent || event?.expiration() != null || geo != null || pow != null + val hasGlyph = note.isPinnedInRelayGroup() || event is EncryptedDmEvent || event?.expiration() != null || geo != null || pow != null if (hasGlyph) Spacer(StdHorzSpacer) // Drafts aren't published, so no relay/delivery detail; everything else gets diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatGroupPosition.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatGroupPosition.kt index d942a54dd9..fbbf82afd3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatGroupPosition.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatGroupPosition.kt @@ -41,7 +41,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent import com.vitorpamplona.quartz.nip53LiveActivities.raid.LiveActivitiesRaidEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import kotlin.math.abs /** @@ -96,7 +96,7 @@ internal const val CHAT_GROUP_WINDOW_SECONDS = 10 * 60L */ private fun isGroupableEvent(event: Event?): Boolean = event != null && - event !is LnZapEvent && + event !is ZapReceiptEvent && event !is LiveActivitiesRaidEvent && event !is LiveActivitiesClipEvent && event !is ChannelCreateEvent && diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt index ee5645fa71..c52b22a690 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt @@ -55,7 +55,7 @@ import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent private const val BIG_ZAP_THRESHOLD_SATS = 50_000L @@ -65,7 +65,7 @@ fun RenderChatZap( accountViewModel: AccountViewModel, nav: INav, ) { - val zapEvent = baseNote.event as? LnZapEvent ?: return + val zapEvent = baseNote.event as? ZapReceiptEvent ?: return val card by produceState( ZapAmountCommentNotification(user = null, comment = null, amount = null), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatViewModel.kt index e653bad481..7a40654504 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/geohashChat/GeohashChatViewModel.kt @@ -29,7 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -122,7 +122,7 @@ class GeohashChatViewModel : ViewModel() { val signer = NostrSignerInternal(keyPair) val template = if (mine.isNotEmpty()) { - DeletionEvent.build(mine) + DeletionRequestEvent.build(mine) } else { val hint = note.toEventHint<Event>() ?: return@launch ReactionEvent.build(reaction, hint) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt index 4a9c15f240..14f7c0528b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt @@ -71,7 +71,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.paging.RelayPagingProgress import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.delay @@ -130,7 +130,7 @@ fun ChatroomView( // Reactively check if recipients have DM relays for NIP-17 delivery for (userHex in room.users) { LoadAddressableNote( - ChatMessageRelayListEvent.createAddress(userHex), + DmRelayListEvent.createAddress(userHex), ) { note -> if (note != null) { EventFinderFilterAssemblerSubscription(note, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt index c203189fd2..2254624aee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent /** @@ -114,7 +114,7 @@ private fun toMeFilter( filter = ExplainedFilter( purpose = SubPurpose.DIRECT_MESSAGES, - kinds = listOf(PrivateDmEvent.KIND), + kinds = listOf(EncryptedDmEvent.KIND), authors = authors.toList(), tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)), since = since, @@ -135,7 +135,7 @@ private fun fromMeFilter( filter = ExplainedFilter( purpose = SubPurpose.DIRECT_MESSAGES, - kinds = listOf(PrivateDmEvent.KIND), + kinds = listOf(EncryptedDmEvent.KIND), authors = listOf(account.userProfile().pubkeyHex), tags = mapOf("p" to pTags.toList()), since = since, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 3bb0e5dce0..ca797a2b12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -68,7 +68,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip01Core.tags.references.references -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip10Notes.content.findHashtags import com.vitorpamplona.quartz.nip10Notes.content.findNostrEventUris import com.vitorpamplona.quartz.nip10Notes.content.findURLs @@ -78,7 +78,7 @@ import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip30CustomEmoji.emojis @@ -184,7 +184,7 @@ class ChatNewMessageViewModel : combine(dmRelayListNoteFlows) { dmRelayListNotes -> dmRelayListNotes .mapNotNull { noteState -> - val noteEvent = noteState.note.event as? ChatMessageRelayListEvent + val noteEvent = noteState.note.event as? DmRelayListEvent if (noteEvent == null || noteEvent.relays().isEmpty()) { noteState.note.author } else { @@ -409,7 +409,7 @@ class ChatNewMessageViewModel : if (replyId != null) { replyTo.value = LocalCache.checkGetOrCreateNote(replyId) } - } else if (draftEvent is PrivateDmEvent) { + } else if (draftEvent is EncryptedDmEvent) { val recipientNPub = draftEvent.verifiedRecipientPubKey()?.let { Hex.decode(it).toNpub() } toUsers.setTextAndPlaceCursorAtEnd("@$recipientNPub") @@ -420,7 +420,7 @@ class ChatNewMessageViewModel : } val draftText = - if (draftEvent is PrivateDmEvent) { + if (draftEvent is EncryptedDmEvent) { accountViewModel.account.privateDMDecryptionCache.cachedDM(draftEvent) ?: "" } else { draftEvent.content diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamGoalHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamGoalHeader.kt index 8ff9d9a3fd..c8f2739522 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamGoalHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamGoalHeader.kt @@ -48,7 +48,7 @@ import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.note.types.GoalProgressBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent +import com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent /** * Compact header rendered above the live-activity chat feed when the stream @@ -76,7 +76,7 @@ private fun GoalHeaderContent( accountViewModel: AccountViewModel, nav: INav, ) { - val goal = goalNote.event as? GoalEvent ?: return + val goal = goalNote.event as? ZapGoalEvent ?: return val title = remember(goal) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt index ad3d06dcbd..1ba516916f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt @@ -43,7 +43,7 @@ import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent @@ -59,8 +59,8 @@ class ChatroomListKnownFeedFilter( private fun isEnabled(type: ChatFeedType): Boolean = type in account.settings.enabledChatFeeds.value - /** A room note is NIP-04 when its event is a [PrivateDmEvent], otherwise it is a NIP-17 message. */ - private fun isDmEnabled(note: Note): Boolean = isEnabled(if (note.event is PrivateDmEvent) ChatFeedType.NIP04 else ChatFeedType.NIP17) + /** A room note is NIP-04 when its event is a [EncryptedDmEvent], otherwise it is a NIP-17 message. */ + private fun isDmEnabled(note: Note): Boolean = isEnabled(if (note.event is EncryptedDmEvent) ChatFeedType.NIP04 else ChatFeedType.NIP17) // returns the last Note of each user. override fun feed(): List<Note> { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt index 86acd5c0cf..14ef60fd83 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.commons.model.chats.ChatFeedType import com.vitorpamplona.amethyst.commons.util.replace import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable @@ -38,8 +38,8 @@ class ChatroomListNewFeedFilter( private fun isEnabled(type: ChatFeedType): Boolean = type in account.settings.enabledChatFeeds.value - /** A room note is NIP-04 when its event is a [PrivateDmEvent], otherwise it is a NIP-17 message. */ - private fun isDmEnabled(note: Note): Boolean = isEnabled(if (note.event is PrivateDmEvent) ChatFeedType.NIP04 else ChatFeedType.NIP17) + /** A room note is NIP-04 when its event is a [EncryptedDmEvent], otherwise it is a NIP-17 message. */ + private fun isDmEnabled(note: Note): Boolean = isEnabled(if (note.event is EncryptedDmEvent) ChatFeedType.NIP04 else ChatFeedType.NIP17) // returns the last Note of each user. override fun feed(): List<Note> { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt index 534ddcbb6e..516c98b011 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFil import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent fun filterNip04DMsFromMe( user: User, @@ -39,7 +39,7 @@ fun filterNip04DMsFromMe( filter = ExplainedFilter( purpose = SubPurpose.DIRECT_MESSAGES, - kinds = listOf(PrivateDmEvent.KIND), + kinds = listOf(EncryptedDmEvent.KIND), authors = listOf(user.pubkeyHex), since = since, until = until, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt index 4ebe2b0077..d7b68ba331 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFil import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent fun filterNip04DMsToMe( user: User, @@ -39,7 +39,7 @@ fun filterNip04DMsToMe( filter = ExplainedFilter( purpose = SubPurpose.DIRECT_MESSAGES, - kinds = listOf(PrivateDmEvent.KIND), + kinds = listOf(EncryptedDmEvent.KIND), tags = mapOf("p" to listOf(user.pubkeyHex)), since = since, until = until, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt index eefcbb1888..502d424d8c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt @@ -70,7 +70,7 @@ import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.paging.RelayPagingProgress import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent @@ -180,8 +180,8 @@ private fun FeedLoaded( val user = accountViewModel.userProfile() val nip17Name = stringRes(Res.string.chats_history_proto_nip17) val nip04Name = stringRes(Res.string.chats_history_proto_nip04) - val oldestNip17Index = items.list.indexOfLast { it.event is ChatroomKeyable && it.event !is PrivateDmEvent } - val oldestNip04Index = items.list.indexOfLast { it.event is PrivateDmEvent } + val oldestNip17Index = items.list.indexOfLast { it.event is ChatroomKeyable && it.event !is EncryptedDmEvent } + val oldestNip04Index = items.list.indexOfLast { it.event is EncryptedDmEvent } // Each relay's window limit, carrying the advance() that pulls its OWN next page. Placed in the list // at its reached depth as a sentinel (see RelayReachMarkers): a relay pages only while its diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt index dc20c61fcd..b6eb04d860 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt @@ -43,9 +43,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivitie import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.RenderCommunitiesThumb import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.RenderContentDVMThumb import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.RenderClassifiedsThumb -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent @@ -157,8 +157,8 @@ fun InnerChannelCardWithReactions( is ChannelCreateEvent -> InnerCardRow(baseNote, accountViewModel, nav) is ClassifiedsEvent -> InnerCardBox(baseNote, accountViewModel, nav) is AppDefinitionEvent -> InnerCardRow(baseNote, accountViewModel, nav) - is FollowListEvent -> InnerCardRow(baseNote, accountViewModel, nav) - is LongTextNoteEvent -> InnerCardRow(baseNote, accountViewModel, nav) + is StarterPackEvent -> InnerCardRow(baseNote, accountViewModel, nav) + is LongFormContentEvent -> InnerCardRow(baseNote, accountViewModel, nav) } } @@ -211,7 +211,7 @@ private fun RenderNoteRow( is CommunityDefinitionEvent -> RenderCommunitiesThumb(baseNote, accountViewModel, nav) is ChannelCreateEvent -> RenderPublicChatChannelThumb(baseNote, accountViewModel, nav) is AppDefinitionEvent -> RenderContentDVMThumb(baseNote, accountViewModel, nav) - is FollowListEvent -> RenderFollowSetThumb(baseNote, accountViewModel, nav) - is LongTextNoteEvent -> RenderLongFormThumb(baseNote, accountViewModel, nav) + is StarterPackEvent -> RenderFollowSetThumb(baseNote, accountViewModel, nav) + is LongFormContentEvent -> RenderLongFormThumb(baseNote, accountViewModel, nav) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt index bb061315f3..5d28a436a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt @@ -86,9 +86,9 @@ import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.TabItem -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent @@ -140,14 +140,14 @@ fun DiscoverScreen( discoveryFollowSetsFeedContentState, "DiscoverFollowSets", ScrollStateKeys.DISCOVER_FOLLOWS, - FollowListEvent.KIND, + StarterPackEvent.KIND, ), TabItem( Res.string.discover_reads, discoveryReadsFeedContentState, "DiscoverReads", ScrollStateKeys.DISCOVER_READS, - LongTextNoteEvent.KIND, + LongFormContentEvent.KIND, ), TabItem( Res.string.discover_content_v2, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt index 4ac6cca8ed..33fdf104ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent open class DiscoverLongFormFeedFilter( val account: Account, @@ -50,9 +50,9 @@ open class DiscoverLongFormFeedFilter( override fun feed(): List<Note> { val params = buildFilterParams(account) val notes = - LocalCache.addressables.filterIntoSet(LongTextNoteEvent.KIND) { _, it -> + LocalCache.addressables.filterIntoSet(LongFormContentEvent.KIND) { _, it -> val noteEvent = it.event - noteEvent is LongTextNoteEvent && params.match(noteEvent, it.relays) + noteEvent is LongFormContentEvent && params.match(noteEvent, it.relays) } return sort(notes) } @@ -70,7 +70,7 @@ open class DiscoverLongFormFeedFilter( return collection.filterTo(HashSet()) { val noteEvent = it.event - noteEvent is LongTextNoteEvent && params.match(noteEvent, it.relays) + noteEvent is LongFormContentEvent && params.match(noteEvent, it.relays) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt index 226c1c0675..e780927f4a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.note.types.LongFormHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent @Composable fun RenderLongFormThumb( @@ -35,7 +35,7 @@ fun RenderLongFormThumb( accountViewModel: AccountViewModel, nav: INav, ) { - val noteEvent by observeNoteEvent<LongTextNoteEvent>(baseNote, accountViewModel) + val noteEvent by observeNoteEvent<LongFormContentEvent>(baseNote, accountViewModel) noteEvent?.let { LongFormHeader( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt index d03ab0074b..e076080373 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt @@ -91,7 +91,7 @@ import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris import com.vitorpamplona.quartz.nip10Notes.content.findURLs import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip30CustomEmoji.emojis import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason @@ -266,7 +266,7 @@ class LongFormPostViewModel : } else { val noteEvent = version?.event - if (noteEvent is LongTextNoteEvent) { + if (noteEvent is LongFormContentEvent) { title = TextFieldValue(noteEvent.title() ?: "") summary = TextFieldValue(noteEvent.summary() ?: "") publishedAt = noteEvent.publishedAt() ?: noteEvent.createdAt @@ -300,12 +300,12 @@ class LongFormPostViewModel : private fun loadFromDraft(draft: Note) { val draftEvent = draft.event ?: return - if (draftEvent is LongTextNoteEvent) { + if (draftEvent is LongFormContentEvent) { loadFromDraft(draftEvent) } } - private fun loadFromDraft(draftEvent: LongTextNoteEvent) { + private fun loadFromDraft(draftEvent: LongFormContentEvent) { title = TextFieldValue(draftEvent.title() ?: "") summary = TextFieldValue(draftEvent.summary() ?: "") publishedAt = draftEvent.publishedAt() ?: draftEvent.createdAt @@ -441,7 +441,7 @@ class LongFormPostViewModel : val contentWarningReason = if (wantsToMarkAsSensitive) contentWarningDescription else null val localExpirationDate = if (wantsExpirationDate) expirationDate else null - return LongTextNoteEvent.build( + return LongFormContentEvent.build( description = tagger.message, title = title.text.trim(), summary = summary.text.trim().ifBlank { null }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt index 13de523110..2e09975f62 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent open class DiscoverFollowSetsFeedFilter( val account: Account, @@ -49,9 +49,9 @@ open class DiscoverFollowSetsFeedFilter( val params = buildFilterParams(account) val notes = - LocalCache.addressables.filterIntoSet(FollowListEvent.KIND) { _, it -> + LocalCache.addressables.filterIntoSet(StarterPackEvent.KIND) { _, it -> val noteEvent = it.event - noteEvent is FollowListEvent && params.match(noteEvent, it.relays) + noteEvent is StarterPackEvent && params.match(noteEvent, it.relays) } return sort(notes) @@ -70,7 +70,7 @@ open class DiscoverFollowSetsFeedFilter( return collection.filterTo(HashSet()) { val noteEvent = it.event - noteEvent is FollowListEvent && params.match(noteEvent, it.relays) + noteEvent is StarterPackEvent && params.match(noteEvent, it.relays) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt index 282e4abe69..d7399581f6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt @@ -63,7 +63,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageBannerBackground import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -82,7 +82,7 @@ fun RenderFollowSetThumb( nav: INav, ) { val card by observeNoteAndMap(baseNote, accountViewModel) { - val noteEvent = it.event as? FollowListEvent + val noteEvent = it.event as? StarterPackEvent FollowSetCard( name = noteEvent?.title()?.ifBlank { null } ?: noteEvent?.dTag() ?: "", @@ -111,7 +111,7 @@ fun RenderFollowSetThumbPreview() { val nav = EmptyNav() val followCard = - FollowListEvent( + StarterPackEvent( id = "eca31634fce7c9068b56fa8db9f387da70bdcceb3986a77ca1a9844f3128eb5f", pubKey = "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da", createdAt = 1761736286, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt index 5bcbfa664a..ef283a4950 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt @@ -92,8 +92,8 @@ import com.vitorpamplona.quartz.lightning.LnInvoiceUtil import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppMetadata -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.status.DvmStatusEvent import kotlinx.coroutines.launch @Composable @@ -190,9 +190,9 @@ fun ObserverContentDiscoveryResponse( val resultFlow = remember(dvmRequestId) { accountViewModel.account.cache - .observeLatestEvent<NIP90ContentDiscoveryResponseEvent>( + .observeLatestEvent<DvmContentDiscoveryResponseEvent>( Filter( - kinds = listOf(NIP90ContentDiscoveryResponseEvent.KIND), + kinds = listOf(DvmContentDiscoveryResponseEvent.KIND), tags = mapOf("e" to listOf(dvmRequestId.idHex)), limit = 1, ), @@ -231,9 +231,9 @@ fun ObserverDvmStatusResponse( val statusFlow = remember(dvmRequestId) { accountViewModel.account.cache - .observeLatestEvent<NIP90StatusEvent>( + .observeLatestEvent<DvmStatusEvent>( Filter( - kinds = listOf(NIP90StatusEvent.KIND), + kinds = listOf(DvmStatusEvent.KIND), tags = mapOf("e" to listOf(dvmRequestId)), limit = 1, ), @@ -258,7 +258,7 @@ fun ObserverDvmStatusResponse( fun PrepareViewContentDiscoveryModels( dvm: User, dvmRequestId: String, - latestResponse: NIP90ContentDiscoveryResponseEvent, + latestResponse: DvmContentDiscoveryResponseEvent, onRefresh: () -> Unit, accountViewModel: AccountViewModel, nav: INav, @@ -303,7 +303,7 @@ fun RenderNostrNIP90ContentDiscoveryScreen( @Composable fun FeedDVM( appDefinitionNote: Note, - latestStatus: NIP90StatusEvent, + latestStatus: DvmStatusEvent, accountViewModel: AccountViewModel, nav: INav, ) { @@ -374,7 +374,7 @@ fun FeedDVM( @Composable fun DvmPaymentActions( - latestStatus: NIP90StatusEvent, + latestStatus: DvmStatusEvent, accountViewModel: AccountViewModel, nav: INav, onStatusUpdate: (String) -> Unit, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/FavoriteAlgoFeedToggle.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/FavoriteAlgoFeedToggle.kt index 1f0670563d..2eb0e01e0b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/FavoriteAlgoFeedToggle.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/FavoriteAlgoFeedToggle.kt @@ -38,7 +38,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.DvmContentDiscoveryRequestEvent /** * Inline star toggle that follows / unfollows a NIP-90 content-discovery DVM. @@ -60,7 +60,7 @@ fun FavoriteAlgoFeedToggle( ) { val supportsContentDiscovery by observeNoteAndMap(appDefinitionNote, accountViewModel) { note -> - (note.event as? AppDefinitionEvent)?.includeKind(NIP90ContentDiscoveryRequestEvent.KIND) == true + (note.event as? AppDefinitionEvent)?.includeKind(DvmContentDiscoveryRequestEvent.KIND) == true } if (!supportsContentDiscovery) return diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt index 286a7e321a..acb70c2ab0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.commons.model.observables.CreatedAtComparator import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent open class NIP90ContentDiscoveryResponseFilter( val account: Account, @@ -40,7 +40,7 @@ open class NIP90ContentDiscoveryResponseFilter( fun acceptableEvent(note: Note): Boolean { val noteEvent = note.event - return noteEvent is NIP90ContentDiscoveryResponseEvent && noteEvent.isTaggedEvent(request) + return noteEvent is DvmContentDiscoveryResponseEvent && noteEvent.isTaggedEvent(request) } override fun feed(): List<Note> { @@ -54,7 +54,7 @@ open class NIP90ContentDiscoveryResponseFilter( comparator = CreatedAtComparator, ) - val noteEvent = latestNote?.event as? NIP90ContentDiscoveryResponseEvent ?: return listOf() + val noteEvent = latestNote?.event as? DvmContentDiscoveryResponseEvent ?: return listOf() return noteEvent.innerTags().mapNotNull { LocalCache.checkGetOrCreateNote(it) @@ -78,7 +78,7 @@ open class NIP90ContentDiscoveryResponseFilter( latestNote = maxNote } - val noteEvent = latestNote?.event as? NIP90ContentDiscoveryResponseEvent ?: return setOf() + val noteEvent = latestNote?.event as? DvmContentDiscoveryResponseEvent ?: return setOf() return noteEvent .innerTags() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/MyEmojiListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/MyEmojiListScreen.kt index 6f5feccd25..6226d1a680 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/MyEmojiListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/MyEmojiListScreen.kt @@ -66,7 +66,7 @@ import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiListEvent import com.vitorpamplona.quartz.nip30CustomEmoji.taggedEmojis // Screen that lets the logged-in user inspect the contents of their kind 10030 selection @@ -102,7 +102,7 @@ private fun MyEmojiListView( TopBarWithBackButton(caption = stringRes(Res.string.my_emoji_list_title), nav) }, ) { contentPadding -> - val packAddresses by observeNoteEventAndMap<EmojiPackSelectionEvent, List<Address>>( + val packAddresses by observeNoteEventAndMap<EmojiListEvent, List<Address>>( selectionNote, accountViewModel, ) { event -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt index 5d9412c8fb..e72a91d271 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt @@ -85,7 +85,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal.Follow import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal.FollowPackMembersUserFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource.FollowPackFeedFilterAssemblerSubscription import com.vitorpamplona.quartz.nip01Core.core.Address -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -309,7 +309,7 @@ private fun DisplayBanner( modifier: Modifier = Modifier, accountViewModel: AccountViewModel, ) { - val noteEvent by observeNoteEvent<FollowListEvent>(baseNote, accountViewModel) + val noteEvent by observeNoteEvent<StarterPackEvent>(baseNote, accountViewModel) noteEvent?.image()?.let { AsyncImage( @@ -327,7 +327,7 @@ fun FollowPackHeader( accountViewModel: AccountViewModel, nav: INav, ) { - val noteEvent by observeNoteEvent<FollowListEvent>(baseNote, accountViewModel) + val noteEvent by observeNoteEvent<StarterPackEvent>(baseNote, accountViewModel) Text( text = noteEvent?.title() ?: baseNote.dTag(), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt index 352fc2da65..663afb6994 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt @@ -37,7 +37,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent import com.vitorpamplona.quartz.nipA4PublicMessages.PublicMessageEvent @@ -62,7 +62,7 @@ class FollowPackFeedConversationsFeedFilter( ) } - val followPackEvent = followPackNote.event as? FollowListEvent + val followPackEvent = followPackNote.event as? StarterPackEvent val follows = followPackEvent?.followIdSet() ?: emptySet() override fun applyFilter(newItems: Set<Note>): Set<Note> = innerApplyFilter(newItems) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt index e721fbdb88..c89f21f3d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt @@ -45,9 +45,9 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent @@ -66,16 +66,16 @@ class FollowPackFeedNewThreadFeedFilter( MusicPlaylistEvent.KIND, PodcastMetadataEvent.KIND, InteractiveStoryPrologueEvent.KIND, - WikiNoteEvent.KIND, + WikiArticleEvent.KIND, NipTextEvent.KIND, ClassifiedsEvent.KIND, FundraiserEvent.KIND, BirdexEvent.KIND, - LongTextNoteEvent.KIND, + LongFormContentEvent.KIND, ) } - val followPackEvent = followPackNote.event as? FollowListEvent + val followPackEvent = followPackNote.event as? StarterPackEvent val follows = followPackEvent?.followIdSet() ?: emptySet() override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followPackNote.idHex @@ -143,8 +143,8 @@ class FollowPackFeedNewThreadFeedFilter( noteEvent is BirdexEvent || noteEvent is BirdDetectionEvent || noteEvent.isRenderableRepost() || - (noteEvent is LongTextNoteEvent && noteEvent.content.isNotEmpty()) || - (noteEvent is WikiNoteEvent && noteEvent.content.isNotEmpty()) || + (noteEvent is LongFormContentEvent && noteEvent.content.isNotEmpty()) || + (noteEvent is WikiArticleEvent && noteEvent.content.isNotEmpty()) || noteEvent is ZapPollEvent || noteEvent is HighlightEvent || noteEvent is InteractiveStoryPrologueEvent || diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt index 033edb6744..a081c0031b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache.checkGetOrCreateUser import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent class FollowPackMembersFeedFilter( val followPackNote: AddressableNote, @@ -33,10 +33,10 @@ class FollowPackMembersFeedFilter( ) : FeedFilter<User>() { override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followPackNote.idHex - val cache: MutableMap<FollowListEvent, List<User>> = mutableMapOf() + val cache: MutableMap<StarterPackEvent, List<User>> = mutableMapOf() override fun feed(): List<User> { - val followPackEvent = followPackNote.event as? FollowListEvent ?: return emptyList() + val followPackEvent = followPackNote.event as? StarterPackEvent ?: return emptyList() val previousList = cache[followPackEvent] if (previousList != null) return previousList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterSubAssembler.kt index d392e9ceb3..7c7b968467 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterSubAssembler.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.home.nip65Follows.filterHo import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent class FollowPackFeedFilterSubAssembler( client: INostrClient, @@ -40,7 +40,7 @@ class FollowPackFeedFilterSubAssembler( if (keys.isEmpty()) return emptyList() return keys.flatMap { val followPack = it.followPack.event - if (followPack is FollowListEvent) { + if (followPack is StarterPackEvent) { val filter = if (it.account.proxyRelayList.flow.value .isEmpty() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksFeedLoaded.kt index 9b77a2d811..4ff1d00053 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksFeedLoaded.kt @@ -38,7 +38,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.ChannelCardCompose -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent @OptIn(ExperimentalFoundationApi::class) @Composable @@ -64,7 +64,7 @@ fun FollowPacksFeedLoaded( baseNote = item, routeForLastRead = "FollowPacksFeed", modifier = Modifier.fillMaxWidth(), - forceEventKind = FollowListEvent.KIND, + forceEventKind = StarterPackEvent.KIND, accountViewModel = accountViewModel, nav = nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksTopBar.kt index 4775dd8a8c..1c67d0241b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksTopBar.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar import com.vitorpamplona.amethyst.ui.screen.FeedDefinition import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent @Composable fun FollowPacksTopBar( @@ -49,7 +49,7 @@ fun FollowPacksTopBar( // The feed's own kind window, plus whatever the list spinner narrowed it to — a hashtag // or a geohash says itself as a token; a follow set does not, and seeds nothing. val me = accountViewModel.userProfile().pubkeyHex - val seed = remember(list, me) { SearchSeed.merge(SearchSeed.ofKinds(FollowListEvent.KIND), list.asSearchQuery(me)) } + val seed = remember(list, me) { SearchSeed.merge(SearchSeed.ofKinds(StarterPackEvent.KIND), list.asSearchQuery(me)) } UserDrawerSearchTopBar(accountViewModel, nav, seed) { FollowPacksTopNavFilterBar( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt index 5e3caa267d..3322db8e7d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt @@ -34,12 +34,12 @@ import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHash -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId import com.vitorpamplona.quartz.nipF4Podcasts.episode.PodcastEpisodeEvent import com.vitorpamplona.quartz.nipF4Podcasts.metadata.PodcastMetadataEvent @@ -79,10 +79,10 @@ class GeoHashFeedFilter( ): Boolean = ( event is TextNoteEvent || - event is LongTextNoteEvent || - event is WikiNoteEvent || + event is LongFormContentEvent || + event is WikiArticleEvent || event is ChannelMessageEvent || - event is PrivateDmEvent || + event is EncryptedDmEvent || event is ZapPollEvent || event is AudioHeaderEvent || event is MusicTrackEvent || diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt index 99c78942fd..0c7d4f80cc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt @@ -34,12 +34,12 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId import com.vitorpamplona.quartz.nipF4Podcasts.episode.PodcastEpisodeEvent import com.vitorpamplona.quartz.nipF4Podcasts.metadata.PodcastMetadataEvent @@ -104,10 +104,10 @@ class HashtagFeedFilter( ( event is TextNoteEvent || event.isRenderableRepost() || - event is LongTextNoteEvent || - event is WikiNoteEvent || + event is LongFormContentEvent || + event is WikiArticleEvent || event is ChannelMessageEvent || - event is PrivateDmEvent || + event is EncryptedDmEvent || event is ZapPollEvent || event is AudioHeaderEvent || event is MusicTrackEvent || diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt index bfd839f1fa..2586985599 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt @@ -36,10 +36,10 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent @@ -51,12 +51,12 @@ val PostsByHashtagsKinds = listOf( TextNoteEvent.KIND, ChannelMessageEvent.KIND, - LongTextNoteEvent.KIND, + LongFormContentEvent.KIND, PollEvent.KIND, LiveActivitiesChatMessageEvent.KIND, ClassifiedsEvent.KIND, HighlightEvent.KIND, - WikiNoteEvent.KIND, + WikiArticleEvent.KIND, CommentEvent.KIND, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt index 3297759834..1cf99188dd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt @@ -49,16 +49,16 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip64Chess.end.LiveChessGameEndEvent import com.vitorpamplona.quartz.nip64Chess.game.ChessGameEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent @@ -77,15 +77,15 @@ class HomeNewThreadFeedFilter( MusicPlaylistEvent.KIND, PodcastMetadataEvent.KIND, InteractiveStoryPrologueEvent.KIND, - WikiNoteEvent.KIND, + WikiArticleEvent.KIND, ClassifiedsEvent.KIND, FundraiserEvent.KIND, BirdexEvent.KIND, - LongTextNoteEvent.KIND, + LongFormContentEvent.KIND, LiveChessGameEndEvent.KIND, AttestationEvent.KIND, - VideoHorizontalEvent.KIND, - VideoVerticalEvent.KIND, + AddressableNormalVideoEvent.KIND, + AddressableShortVideoEvent.KIND, EntityRatingEvent.KIND, ) } @@ -147,8 +147,8 @@ class HomeNewThreadFeedFilter( noteEvent is BirdexEvent || noteEvent is BirdDetectionEvent || noteEvent.isRenderableRepost() || - (noteEvent is LongTextNoteEvent && noteEvent.content.isNotEmpty()) || - (noteEvent is WikiNoteEvent && noteEvent.content.isNotEmpty()) || + (noteEvent is LongFormContentEvent && noteEvent.content.isNotEmpty()) || + (noteEvent is WikiArticleEvent && noteEvent.content.isNotEmpty()) || noteEvent is ZapPollEvent || noteEvent is PollEvent || noteEvent is HighlightEvent || @@ -166,8 +166,8 @@ class HomeNewThreadFeedFilter( noteEvent is PictureEvent || noteEvent is VideoNormalEvent || noteEvent is VideoShortEvent || - noteEvent is VideoHorizontalEvent || - noteEvent is VideoVerticalEvent || + noteEvent is AddressableNormalVideoEvent || + noteEvent is AddressableShortVideoEvent || noteEvent is TorrentEvent || noteEvent is AttestationEvent || noteEvent is AttestationRequestEvent || diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/nip75Goals/NewGoalViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/nip75Goals/NewGoalViewModel.kt index 22aac53186..1696cdae66 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/nip75Goals/NewGoalViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/nip75Goals/NewGoalViewModel.kt @@ -32,7 +32,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate -import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent +import com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent import com.vitorpamplona.quartz.utils.TimeUtils @Stable @@ -89,7 +89,7 @@ class NewGoalViewModel : ViewModel() { val sum = summary.text.ifBlank { null } val web = websiteUrl.text.ifBlank { null } - return GoalEvent.build( + return ZapGoalEvent.build( description = description.text, amount = amountMillisats, relays = relays, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt index dc141e4b72..8733e7a42f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -62,7 +62,7 @@ class PeopleListViewModel : ViewModel() { }.flowOn(Dispatchers.IO) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null) - fun selectedAddress() = PeopleListEvent.createAddress(account.userProfile().pubkeyHex, selectedDTag.value) + fun selectedAddress() = FollowSetEvent.createAddress(account.userProfile().pubkeyHex, selectedDTag.value) fun selectedNote() = account.cache.getOrCreateAddressableNote(selectedAddress()) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt index 62948d2075..f54fa456ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -62,7 +62,7 @@ class FollowPackViewModel : ViewModel() { }.flowOn(Dispatchers.IO) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null) - fun selectedAddress() = FollowListEvent.createAddress(account.userProfile().pubkeyHex, selectedDTag.value) + fun selectedAddress() = StarterPackEvent.createAddress(account.userProfile().pubkeyHex, selectedDTag.value) fun selectedNote() = account.cache.getOrCreateAddressableNote(selectedAddress()) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsTopBar.kt index 9e87884561..898057185a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/LongsTopBar.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar import com.vitorpamplona.amethyst.ui.screen.FeedDefinition import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent @Composable fun LongsTopBar( @@ -49,7 +49,7 @@ fun LongsTopBar( // The feed's own kind window, plus whatever the list spinner narrowed it to — a hashtag // or a geohash says itself as a token; a follow set does not, and seeds nothing. val me = accountViewModel.userProfile().pubkeyHex - val seed = remember(list, me) { SearchSeed.merge(SearchSeed.ofKinds(VideoHorizontalEvent.KIND), list.asSearchQuery(me)) } + val seed = remember(list, me) { SearchSeed.merge(SearchSeed.ofKinds(AddressableNormalVideoEvent.KIND), list.asSearchQuery(me)) } UserDrawerSearchTopBar(accountViewModel, nav, seed) { LongsTopNavFilterBar( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/dal/LongsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/dal/LongsFeedFilter.kt index cfe0cbc3b5..138d6b8e34 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/dal/LongsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/dal/LongsFeedFilter.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal.SupportedContent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoMeta import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent @@ -60,7 +60,7 @@ class LongsFeedFilter( LocalCache.notes.filterIntoSet { _, it -> acceptableEvent(it, params) } + - LocalCache.addressables.filterIntoSet(VideoHorizontalEvent.KIND) { _, it -> + LocalCache.addressables.filterIntoSet(AddressableNormalVideoEvent.KIND) { _, it -> acceptableEvent(it, params) } return sort(notes) @@ -91,7 +91,7 @@ class LongsFeedFilter( fun acceptableVideoiMetas(iMetas: List<VideoMeta>): Boolean = iMetas.any { videoFeedSupport.acceptableUrl(it.url, it.mimeType) } - fun acceptanceEvent(noteEvent: VideoHorizontalEvent) = acceptableVideoiMetas(noteEvent.imetaTags()) + fun acceptanceEvent(noteEvent: AddressableNormalVideoEvent) = acceptableVideoiMetas(noteEvent.imetaTags()) fun acceptanceEvent(noteEvent: VideoNormalEvent) = acceptableVideoiMetas(noteEvent.imetaTags()) @@ -106,7 +106,7 @@ class LongsFeedFilter( } return ( - (noteEvent is VideoHorizontalEvent && acceptanceEvent(noteEvent)) || + (noteEvent is AddressableNormalVideoEvent && acceptanceEvent(noteEvent)) || (noteEvent is VideoNormalEvent && acceptanceEvent(noteEvent)) ) && params.match(noteEvent, note.relays) && diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/NestRoomFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/NestRoomFilterAssembler.kt index 3923973d8f..ca4c53d621 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/NestRoomFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/datasource/NestRoomFilterAssembler.kt @@ -41,7 +41,7 @@ import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent /** @@ -100,7 +100,7 @@ class NestRoomFilterSubAssembler( LiveActivitiesChatMessageEvent.KIND, MeetingRoomPresenceEvent.KIND, ReactionEvent.KIND, - LnZapEvent.KIND, + ZapReceiptEvent.KIND, Bolt12ZapEvent.KIND, ), tags = mapOf("a" to listOf(key.note.idHex)), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lifecycle/NestRoomEventCollectors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lifecycle/NestRoomEventCollectors.kt index 6e3fb4d01b..a18200cfdd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lifecycle/NestRoomEventCollectors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/lifecycle/NestRoomEventCollectors.kt @@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.delay @@ -153,7 +153,7 @@ private fun ReactionsCollector( * floating zap-overlay aggregator. The same note flows two places: * * 1. [NestViewModel.onChatEvent] — `ChatroomMessageCompose` routes - * `LnZapEvent` notes through `RenderChatZap`, which is the same + * `ZapReceiptEvent` notes through `RenderChatZap`, which is the same * card live streams use. Sharing the chat ledger keeps zap * cards interleaved with kind-1311 chat messages in time order. * @@ -169,14 +169,14 @@ private fun ZapsCollector( LaunchedEffect(viewModel, roomATag) { val filter = Filter( - kinds = listOf(LnZapEvent.KIND, Bolt12ZapEvent.KIND), + kinds = listOf(ZapReceiptEvent.KIND, Bolt12ZapEvent.KIND), tags = mapOf("a" to listOf(roomATag)), ) LocalCache.observeNotes(filter).collect { notes -> val nowSec = System.currentTimeMillis() / 1000 notes.forEach { note -> viewModel.onChatEvent(note) - (note.event as? LnZapEvent)?.let { viewModel.onZapEvent(it, nowSec) } + (note.event as? ZapReceiptEvent)?.let { viewModel.onZapEvent(it, nowSec) } (note.event as? Bolt12ZapEvent)?.let { viewModel.onZapEvent(it, nowSec) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt index 25f3230185..a219448a67 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt @@ -45,14 +45,14 @@ import com.vitorpamplona.amethyst.ui.dal.NotificationFeedOrderCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal.NotificationFeedFilter import com.vitorpamplona.quartz.buzz.notifications.MemberAddedNotificationEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent import com.vitorpamplona.quartz.utils.Log @@ -183,7 +183,7 @@ class CardFeedContentState( val zapsPerUser = mutableMapOf<User, MutableList<CombinedZap>>() val zapsPerEvent = mutableMapOf<Note, MutableList<CombinedZap>>() notes - .filter { it.event is LnZapEvent } + .filter { it.event is ZapReceiptEvent } .forEach { zapEvent -> val zappedPost = zapEvent.replyTo?.lastOrNull() if (zappedPost != null) { @@ -200,7 +200,7 @@ class CardFeedContentState( .add(CombinedZap(zapRequest, zapEvent)) } } else { - val event = (zapEvent.event as LnZapEvent) + val event = (zapEvent.event as ZapReceiptEvent) val author = event.zappedAuthor().firstNotNullOfOrNull { LocalCache.getUserIfExists(it) // don't create user if it doesn't exist @@ -361,7 +361,7 @@ class CardFeedContentState( it.event !is ReactionEvent && it.event !is RepostEvent && it.event !is GenericRepostEvent && - it.event !is LnZapEvent && + it.event !is ZapReceiptEvent && // Nutzaps are already grouped into nutzapsPerEvent (MultiSetCard) // or nutzapsPerUser (NutzapUserSetCard) above, exactly like lightning // zaps. Without this exclusion a nutzap would ALSO fall through to a @@ -372,7 +372,7 @@ class CardFeedContentState( val pendingInvite = if (it.event is MemberAddedNotificationEvent) pendingInvites[it.idHex] else null if (pendingInvite != null) { ChannelInviteCard(it, pendingInvite) - } else if (it.event is PrivateDmEvent || it.event is NIP17Group || it.isInMarmotGroup()) { + } else if (it.event is EncryptedDmEvent || it.event is NIP17Group || it.isInMarmotGroup()) { MessageSetCard(it) } else if (it.event is BadgeAwardEvent) { BadgeCard(it) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt index 8b86a5c5da..e1cc54cc4a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt @@ -39,7 +39,7 @@ import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent import com.vitorpamplona.quartz.utils.Log @@ -117,7 +117,7 @@ class NotificationSummaryState( } } - noteEvent is LnZapEvent -> { + noteEvent is ZapReceiptEvent -> { // the user might be sending his own receipts noteEvent.pubKey != currentUser if (noteEvent.isTaggedUser(currentUser)) { val netDate = formatDate(noteEvent.createdAt) @@ -212,7 +212,7 @@ class NotificationSummaryState( } } - noteEvent is LnZapEvent -> { + noteEvent is ZapReceiptEvent -> { if (noteEvent.isTaggedUser(currentUser)) { // && noteEvent.pubKey != currentUser User might be sending his own receipts val netDate = formatDate(noteEvent.createdAt) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt index c9735bb746..433d8d8183 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt @@ -52,7 +52,7 @@ import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip10Notes.BaseNoteEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent @@ -60,7 +60,7 @@ import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent @@ -78,17 +78,17 @@ import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent import com.vitorpamplona.quartz.nip52Calendar.rsvp.CalendarRSVPEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent import com.vitorpamplona.quartz.nip64Chess.challenge.accept.LiveChessGameAcceptEvent import com.vitorpamplona.quartz.nip64Chess.move.LiveChessMoveEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent @@ -131,11 +131,11 @@ class NotificationFeedFilter( LiveActivitiesEvent.KIND, LiveChessGameAcceptEvent.KIND, LiveChessMoveEvent.KIND, - LongTextNoteEvent.KIND, + LongFormContentEvent.KIND, NipTextEvent.KIND, - VideoVerticalEvent.KIND, - VideoHorizontalEvent.KIND, - WikiNoteEvent.KIND, + AddressableShortVideoEvent.KIND, + AddressableNormalVideoEvent.KIND, + WikiArticleEvent.KIND, AttestationRequestEvent.KIND, ) @@ -198,7 +198,7 @@ class NotificationFeedFilter( TextNoteEvent.KIND, ReactionEvent.KIND, RepostEvent.KIND, - LnZapEvent.KIND, + ZapReceiptEvent.KIND, NutzapEvent.KIND, OnchainZapEvent.KIND, Bolt12ZapEvent.KIND, @@ -206,7 +206,7 @@ class NotificationFeedFilter( PictureEvent.KIND, PollEvent.KIND, ZapPollEvent.KIND, - PrivateDmEvent.KIND, + EncryptedDmEvent.KIND, PublicMessageEvent.KIND, VideoNormalEvent.KIND, VideoShortEvent.KIND, @@ -322,9 +322,9 @@ class NotificationFeedFilter( // proves the comment targets a zap; the reply's explicit p tag // on the user marks it as theirs. val targetsZapReceipt = - event.hasScopeKind(LnZapEvent.KIND.toString()) || + event.hasScopeKind(ZapReceiptEvent.KIND.toString()) || event.hasScopeKind(Bolt12ZapEvent.KIND.toString()) || - note.replyTo?.any { it.event is LnZapEvent || it.event is Bolt12ZapEvent } == true + note.replyTo?.any { it.event is ZapReceiptEvent || it.event is Bolt12ZapEvent } == true if (targetsZapReceipt && event.isTaggedUser(authorHex)) { return true @@ -578,13 +578,13 @@ class NotificationFeedFilter( ( noteEvent is ChatMessageEvent || noteEvent is ChatMessageEncryptedFileHeaderEvent || - noteEvent is PrivateDmEvent + noteEvent is EncryptedDmEvent ) ) { return false } val notifAuthor = - if (noteEvent is LnZapEvent) { + if (noteEvent is ZapReceiptEvent) { val zapRequest = noteEvent.zapRequest if (zapRequest != null) { if (noteEvent.zapRequest?.isPrivateZap() == true) { @@ -608,7 +608,7 @@ class NotificationFeedFilter( // Reactions/zaps/reposts target a note via `replyTo`, not via thread-root tags, // so isNotInMutedThread on the wrapper event misses them. - if (noteEvent is ReactionEvent || noteEvent is LnZapEvent || noteEvent is Bolt12ZapEvent || + if (noteEvent is ReactionEvent || noteEvent is ZapReceiptEvent || noteEvent is Bolt12ZapEvent || noteEvent is RepostEvent || noteEvent is GenericRepostEvent ) { val target = it.replyTo?.lastOrNull() @@ -679,11 +679,11 @@ class NotificationFeedFilter( // relevance check (tagsAnEventByUser is skipped below); it still scopes // to genuine replies, so unrelated channel chatter never leaks through. return noteEvent?.kind in NOTIFICATION_KINDS && - (noteEvent is LnZapEvent || noteEvent is Bolt12ZapEvent || notifAuthor != loggedInUserHex) && + (noteEvent is ZapReceiptEvent || noteEvent is Bolt12ZapEvent || notifAuthor != loggedInUserHex) && (isChessEvent || isConcord || isReactionToMe || isThreadReplyToMe || filterParams.isGlobal() || notifAuthor == null || filterParams.isAuthorInFollows(notifAuthor)) && (noteEvent?.isTaggedUser(loggedInUserHex) == true || isNotifiablePublicChatReply(it, loggedInUserHex) || isReactionToMe || isThreadReplyToMe) && (filterParams.isHiddenList || notifAuthor == null || !account.isHidden(notifAuthor)) && - (noteEvent !is PrivateDmEvent || !account.isDecryptedContentHidden(noteEvent)) && + (noteEvent !is EncryptedDmEvent || !account.isDecryptedContentHidden(noteEvent)) && // For a Concord note the explicit p-tag above IS the relevance signal (the reply/reaction/ // mention targets me directly), so skip the per-kind heuristic — which for a reaction would // otherwise need my target message already loaded to resolve replyTo. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt index 9ce804d10e..5b8fdcda06 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt @@ -61,7 +61,7 @@ import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import java.math.BigDecimal private val Gold = Color(0xFFFFC300) @@ -86,7 +86,7 @@ fun PodcastTopSupporters( remember(zapState) { val contributions = note.zaps.mapNotNull { (_, receiptNote) -> - val receipt = receiptNote?.event as? LnZapEvent ?: return@mapNotNull null + val receipt = receiptNote?.event as? ZapReceiptEvent ?: return@mapNotNull null val request = receipt.zapRequest ?: return@mapNotNull null val sats = receipt.amount()?.toLong() ?: return@mapNotNull null // Anon/private zaps carry an `anon` tag; collapse them into the shared bucket. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt index b6742d40aa..aef3178ea3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt @@ -32,10 +32,10 @@ import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.AddressableVideoEvent import com.vitorpamplona.quartz.nip71Video.RegularVideoEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent class UserProfileGalleryFeedFilter( val user: User, @@ -57,7 +57,7 @@ class UserProfileGalleryFeedFilter( // Both NIP-71 addressable kinds: acceptableEvent() takes any // AddressableVideoEvent, so listing 34236 twice silently kept every // 34235 (horizontal) video out of the gallery. - listOf(VideoVerticalEvent.KIND, VideoHorizontalEvent.KIND), + listOf(AddressableShortVideoEvent.KIND, AddressableNormalVideoEvent.KIND), user.pubkeyHex, ) { _, it -> acceptableEvent(it, params, user) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt index 4a1654ac55..1af33308ff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt @@ -41,9 +41,9 @@ import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nipA4PublicMessages.PublicMessageEvent @@ -83,8 +83,8 @@ class UserProfileMutualFeedFilter( it.event is BirdexEvent || it.event is BirdDetectionEvent || it.event.isRenderableRepost() || - it.event is LongTextNoteEvent || - it.event is WikiNoteEvent || + it.event is LongFormContentEvent || + it.event is WikiArticleEvent || it.event is NipTextEvent || it.event is ZapPollEvent || it.event is HighlightEvent || diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt index f649abb356..561bc45837 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt @@ -47,9 +47,9 @@ import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent @@ -91,8 +91,8 @@ class UserProfileNewThreadFeedFilter( it.event is BirdexEvent || it.event is BirdDetectionEvent || it.event.isRenderableRepost() || - it.event is LongTextNoteEvent || - it.event is WikiNoteEvent || + it.event is LongFormContentEvent || + it.event is WikiArticleEvent || it.event is NipTextEvent || it.event is ZapPollEvent || it.event is PollEvent || diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentContent.kt index c437afb68d..1bd3ac0ebb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentContent.kt @@ -99,7 +99,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.note.showAmount -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -139,7 +139,7 @@ data class PaymentMethodUi( /** One entry of the zap-type selector (Public / Private / Anonymous / Non-Zap). */ @Immutable data class ZapTypeOption( - val type: LnZapEvent.ZapType, + val type: ZapReceiptEvent.ZapType, val label: String, val explainer: String, ) @@ -270,8 +270,8 @@ fun SendPaymentContent( showMessageField: Boolean, messageLabel: String, zapTypes: ImmutableList<ZapTypeOption>?, - selectedZapType: LnZapEvent.ZapType, - onZapTypeChange: (LnZapEvent.ZapType) -> Unit, + selectedZapType: ZapReceiptEvent.ZapType, + onZapTypeChange: (ZapReceiptEvent.ZapType) -> Unit, receiptNote: String?, stage: PaymentFlowStage, canSend: Boolean, @@ -630,8 +630,8 @@ private fun AmountSection( @Composable private fun ReceiptSection( zapTypes: ImmutableList<ZapTypeOption>?, - selectedZapType: LnZapEvent.ZapType, - onZapTypeChange: (LnZapEvent.ZapType) -> Unit, + selectedZapType: ZapReceiptEvent.ZapType, + onZapTypeChange: (ZapReceiptEvent.ZapType) -> Unit, receiptNote: String?, enabled: Boolean, ) { @@ -771,10 +771,10 @@ private val previewFromSources = private val previewZapTypes = listOf( - ZapTypeOption(LnZapEvent.ZapType.PUBLIC, "Public", "Everybody can see the transaction and message"), - ZapTypeOption(LnZapEvent.ZapType.PRIVATE, "Private", "Sender and receiver can see each other and read the message"), - ZapTypeOption(LnZapEvent.ZapType.ANONYMOUS, "Anonymous", "Receiver does not know who sent the payment"), - ZapTypeOption(LnZapEvent.ZapType.NONZAP, "Non-Zap", "No trace on Nostr, only in Lightning"), + ZapTypeOption(ZapReceiptEvent.ZapType.PUBLIC, "Public", "Everybody can see the transaction and message"), + ZapTypeOption(ZapReceiptEvent.ZapType.PRIVATE, "Private", "Sender and receiver can see each other and read the message"), + ZapTypeOption(ZapReceiptEvent.ZapType.ANONYMOUS, "Anonymous", "Receiver does not know who sent the payment"), + ZapTypeOption(ZapReceiptEvent.ZapType.NONZAP, "Non-Zap", "No trace on Nostr, only in Lightning"), ).toImmutableList() @Composable @@ -822,7 +822,7 @@ private fun SendPaymentLightningPreview() { showMessageField = true, messageLabel = PREVIEW_MESSAGE_LABEL, zapTypes = previewZapTypes, - selectedZapType = LnZapEvent.ZapType.PUBLIC, + selectedZapType = ZapReceiptEvent.ZapType.PUBLIC, onZapTypeChange = {}, receiptNote = null, stage = PaymentFlowStage.Editing, @@ -858,7 +858,7 @@ private fun SendPaymentClinkFixedPreview() { showMessageField = false, messageLabel = "", zapTypes = null, - selectedZapType = LnZapEvent.ZapType.PUBLIC, + selectedZapType = ZapReceiptEvent.ZapType.PUBLIC, onZapTypeChange = {}, receiptNote = "Direct Lightning payment — no zap receipt is published on Nostr.", stage = PaymentFlowStage.Editing, @@ -894,7 +894,7 @@ private fun SendPaymentInProgressPreview() { showMessageField = true, messageLabel = PREVIEW_MESSAGE_LABEL, zapTypes = null, - selectedZapType = LnZapEvent.ZapType.PUBLIC, + selectedZapType = ZapReceiptEvent.ZapType.PUBLIC, onZapTypeChange = {}, receiptNote = "The nutzap event delivers the ecash itself, so it is always published.", stage = PaymentFlowStage.InProgress("Locking and sending ecash…", progress = 0.55f), @@ -930,7 +930,7 @@ private fun SendPaymentSuccessPreview() { showMessageField = true, messageLabel = PREVIEW_MESSAGE_LABEL, zapTypes = previewZapTypes, - selectedZapType = LnZapEvent.ZapType.PUBLIC, + selectedZapType = ZapReceiptEvent.ZapType.PUBLIC, onZapTypeChange = {}, receiptNote = null, stage = PaymentFlowStage.Success("Payment sent!", "1,000 sats to Alice"), @@ -966,7 +966,7 @@ private fun SendPaymentFailurePreview() { showMessageField = true, messageLabel = "Comment", zapTypes = null, - selectedZapType = LnZapEvent.ZapType.PUBLIC, + selectedZapType = ZapReceiptEvent.ZapType.PUBLIC, onZapTypeChange = {}, receiptNote = "An on-chain zap receipt is published on Nostr so the recipient can find the payment.", stage = PaymentFlowStage.Failure("Insufficient funds to cover the amount plus the network fee."), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt index adaec88edf..32bfc5a77a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt @@ -117,7 +117,7 @@ import com.vitorpamplona.quartz.experimental.clink.offers.OfferErrorCode import com.vitorpamplona.quartz.experimental.clink.pointers.ClinkPointerParser import com.vitorpamplona.quartz.experimental.clink.pointers.NOffer import com.vitorpamplona.quartz.experimental.clink.pointers.OfferPriceType -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nipBCOnchainZaps.chain.FeeEstimates import com.vitorpamplona.quartz.nipBCOnchainZaps.taproot.SegwitAddress import com.vitorpamplona.quartz.nipBCOnchainZaps.taproot.TaprootAddress @@ -621,9 +621,9 @@ private fun SendPaymentLoaded( val messageLabel = when { selectedMethod == ProfilePaymentMethod.ONCHAIN -> stringRes(Res.string.note_to_receiver) - selectedMethod == ProfilePaymentMethod.LIGHTNING && zapType == LnZapEvent.ZapType.PRIVATE -> + selectedMethod == ProfilePaymentMethod.LIGHTNING && zapType == ZapReceiptEvent.ZapType.PRIVATE -> stringRes(Res.string.custom_zaps_add_a_message_private) - selectedMethod == ProfilePaymentMethod.LIGHTNING && zapType == LnZapEvent.ZapType.NONZAP -> + selectedMethod == ProfilePaymentMethod.LIGHTNING && zapType == ZapReceiptEvent.ZapType.NONZAP -> stringRes(Res.string.custom_zaps_add_a_message_nonzap) else -> stringRes(Res.string.custom_zaps_add_a_message) } @@ -738,10 +738,10 @@ private fun rememberLightningZapTypeOptions(): ImmutableList<ZapTypeOption> { return remember { persistentListOf( - ZapTypeOption(LnZapEvent.ZapType.PUBLIC, publicLabel, publicExplainer), - ZapTypeOption(LnZapEvent.ZapType.PRIVATE, privateLabel, privateExplainer), - ZapTypeOption(LnZapEvent.ZapType.ANONYMOUS, anonymousLabel, anonymousExplainer), - ZapTypeOption(LnZapEvent.ZapType.NONZAP, nonzapLabel, nonzapExplainer), + ZapTypeOption(ZapReceiptEvent.ZapType.PUBLIC, publicLabel, publicExplainer), + ZapTypeOption(ZapReceiptEvent.ZapType.PRIVATE, privateLabel, privateExplainer), + ZapTypeOption(ZapReceiptEvent.ZapType.ANONYMOUS, anonymousLabel, anonymousExplainer), + ZapTypeOption(ZapReceiptEvent.ZapType.NONZAP, nonzapLabel, nonzapExplainer), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt index cb0e4df73a..3c10216804 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.nip01Core.RelayInfo import com.vitorpamplona.amethyst.commons.model.nip01Core.Wrapper import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.Dispatchers @@ -144,7 +144,7 @@ class RelayFeedViewModel : .flow() .metadata.stateFlow .map { dmNote -> - (dmNote.note.event as? ChatMessageRelayListEvent)?.relays()?.toSet() ?: emptySet() + (dmNote.note.event as? DmRelayListEvent)?.relays()?.toSet() ?: emptySet() }, flow2 = user.relayState().flow(), transform = ::convert, @@ -154,7 +154,7 @@ class RelayFeedViewModel : emit(emptyList()) } }.onStart { - emit(convert((currentUser.value?.nip65RelayListNote?.event as? ChatMessageRelayListEvent)?.relays()?.toSet(), currentUser.value?.relayState()?.data)) + emit(convert((currentUser.value?.nip65RelayListNote?.event as? DmRelayListEvent)?.relays()?.toSet(), currentUser.value?.relayState()?.data)) }.flowOn(Dispatchers.IO) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsViewModel.kt index 4e718ef67e..db71a85712 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsViewModel.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent import com.vitorpamplona.quartz.utils.BigDecimal @@ -55,14 +55,14 @@ class UserProfileZapsViewModel( ) : ViewModel() { val zapsToUser = Filter( - kinds = listOf(LnZapEvent.KIND, OnchainZapEvent.KIND, Bolt12ZapEvent.KIND), + kinds = listOf(ZapReceiptEvent.KIND, OnchainZapEvent.KIND, Bolt12ZapEvent.KIND), tags = mapOf("p" to listOf(user.pubkeyHex)), ) val sortingModel: Comparator<ZapAmount> = compareByDescending<ZapAmount> { it.amount }.thenBy { it.user.pubkeyHex } - suspend fun mapRequest(zapEvent: LnZapEvent): ZapAmount? { + suspend fun mapRequest(zapEvent: ZapReceiptEvent): ZapAmount? { val zapRequest = zapEvent.zapRequest ?: return ZapAmount( LocalCache.getOrCreateUser(zapEvent.pubKey), @@ -122,7 +122,7 @@ class UserProfileZapsViewModel( this.forEach { zapEvent -> val zapAmount = when (zapEvent) { - is LnZapEvent -> mapRequest(zapEvent) + is ZapReceiptEvent -> mapRequest(zapEvent) is OnchainZapEvent -> mapOnchainZap(zapEvent) is Bolt12ZapEvent -> mapBolt12Zap(zapEvent) else -> null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/KindDisplayName.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/KindDisplayName.kt index 2ea4d40bac..0684f76185 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/KindDisplayName.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/KindDisplayName.kt @@ -206,25 +206,25 @@ import com.vitorpamplona.quartz.kinds.KindNames import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelHideMessageEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMuteUserEvent -import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent +import com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiListEvent import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestEvent @@ -239,32 +239,32 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent -import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent +import com.vitorpamplona.quartz.nip38UserStatus.UserStatusEvent import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent +import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.videoCurationSet.VideoCurationSetEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import com.vitorpamplona.quartz.nip52Calendar.rsvp.CalendarRSVPEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent @@ -272,16 +272,16 @@ import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEv import com.vitorpamplona.quartz.nip53LiveActivities.nestsServers.NestsServersEvent import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent @@ -296,29 +296,29 @@ import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.RelayDiscoveryEvent import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.RelayMonitorEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip71Video.textTrack.TextTrackEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent -import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent +import com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent -import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent -import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryRequest.NIP90UserDiscoveryRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryResponse.NIP90UserDiscoveryResponseEvent -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.DvmContentDiscoveryRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.status.DvmStatusEvent +import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryRequest.DvmUserDiscoveryRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryResponse.DvmUserDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent @@ -366,12 +366,12 @@ fun kindDisplayName(kind: Int): StringResource? = BookmarkListEvent.KIND -> Res.string.kind_bookmark_list OldBookmarkListEvent.KIND -> Res.string.kind_old_bookmark_list CalendarDateSlotEvent.KIND -> Res.string.kind_day_appointment - CalendarEvent.KIND -> Res.string.kind_calendar + CalendarCollectionEvent.KIND -> Res.string.kind_calendar CalendarTimeSlotEvent.KIND -> Res.string.kind_appointment CalendarRSVPEvent.KIND -> Res.string.kind_appt_rsvp ChessGameEvent.KIND -> Res.string.kind_chess_games JesterEvent.KIND -> Res.string.kind_chess_auth - RelayFeedsListEvent.KIND -> Res.string.kind_favorite_relays + FavoriteRelayListEvent.KIND -> Res.string.kind_favorite_relays LiveChessGameChallengeEvent.KIND -> Res.string.kind_chess_challenges LiveChessGameAcceptEvent.KIND -> Res.string.kind_chess_game_accept LiveChessMoveEvent.KIND -> Res.string.kind_chess_move @@ -379,32 +379,32 @@ fun kindDisplayName(kind: Int): StringResource? = LiveChessDrawOfferEvent.KIND -> Res.string.kind_chess_draw_offer ChannelCreateEvent.KIND -> Res.string.kind_channel_definition ChannelHideMessageEvent.KIND -> Res.string.kind_channel_hide_msg - ChannelListEvent.KIND -> Res.string.kind_channel_list + PublicChatListEvent.KIND -> Res.string.kind_channel_list ChannelMessageEvent.KIND -> Res.string.kind_channel_message ChannelMetadataEvent.KIND -> Res.string.kind_channel_metadata ChannelMuteUserEvent.KIND -> Res.string.kind_channel_mute_user ChatMessageEncryptedFileHeaderEvent.KIND -> Res.string.kind_dm_file ChatMessageEvent.KIND -> Res.string.kind_dm_message - ChatMessageRelayListEvent.KIND -> Res.string.kind_dm_relays + DmRelayListEvent.KIND -> Res.string.kind_dm_relays ClassifiedsEvent.KIND -> Res.string.kind_classifieds CommentEvent.KIND -> Res.string.kind_comments CommunityDefinitionEvent.KIND -> Res.string.kind_community_def CommunityListEvent.KIND -> Res.string.kind_community_list CommunityPostApprovalEvent.KIND -> Res.string.kind_community_post ContactListEvent.KIND -> Res.string.kind_follow_list - DeletionEvent.KIND -> Res.string.kind_deletions + DeletionRequestEvent.KIND -> Res.string.kind_deletions DraftWrapEvent.KIND -> Res.string.kind_drafts EmojiPackEvent.KIND -> Res.string.kind_emoji_packs - EmojiPackSelectionEvent.KIND -> Res.string.kind_emoji_pack_list + EmojiListEvent.KIND -> Res.string.kind_emoji_pack_list EphemeralChatEvent.KIND -> Res.string.kind_ephemeral_chat EphemeralChatListEvent.KIND -> Res.string.kind_ephemeral_chatrooms - FileHeaderEvent.KIND -> Res.string.kind_file_headers + FileMetadataEvent.KIND -> Res.string.kind_file_headers ProfileGalleryEntryEvent.KIND -> Res.string.kind_profile_gallery FileServersEvent.KIND -> Res.string.kind_file_servers FileStorageEvent.KIND -> Res.string.kind_blob_data FileStorageHeaderEvent.KIND -> Res.string.kind_blob_headers FhirResourceEvent.KIND -> Res.string.kind_medical_data - FollowListEvent.KIND -> Res.string.kind_follow_packs + StarterPackEvent.KIND -> Res.string.kind_follow_packs GenericRepostEvent.KIND -> Res.string.kind_reposts_16 GeohashListEvent.KIND -> Res.string.kind_geohash_follows GiftWrapEvent.KIND -> Res.string.kind_gift_wraps @@ -419,24 +419,24 @@ fun kindDisplayName(kind: Int): StringResource? = GitStatusAppliedEvent.KIND -> Res.string.kind_git_status_applied GitStatusClosedEvent.KIND -> Res.string.kind_git_status_closed GitStatusDraftEvent.KIND -> Res.string.kind_git_status_draft - GoalEvent.KIND -> Res.string.kind_zap_goals - HashtagListEvent.KIND -> Res.string.kind_hashtag_follows + ZapGoalEvent.KIND -> Res.string.kind_zap_goals + InterestListEvent.KIND -> Res.string.kind_hashtag_follows HighlightEvent.KIND -> Res.string.kind_highlights HTTPAuthorizationEvent.KIND -> Res.string.kind_http_auth IndexerRelayListEvent.KIND -> Res.string.kind_index_relay_list InteractiveStoryPrologueEvent.KIND -> Res.string.kind_adventure_prologue InteractiveStorySceneEvent.KIND -> Res.string.kind_adventure_scene InteractiveStoryReadingStateEvent.KIND -> Res.string.kind_adventure_reading - LabeledBookmarkListEvent.KIND -> Res.string.kind_named_bookmarks + BookmarkSetEvent.KIND -> Res.string.kind_named_bookmarks LiveActivitiesChatMessageEvent.KIND -> Res.string.kind_live_chats LiveActivitiesEvent.KIND -> Res.string.kind_live_streams - LnZapEvent.KIND -> Res.string.kind_zaps + ZapReceiptEvent.KIND -> Res.string.kind_zaps Bolt12ZapEvent.KIND -> Res.string.kind_zaps - LnZapPaymentRequestEvent.KIND -> Res.string.kind_nwc_request - LnZapPaymentResponseEvent.KIND -> Res.string.kind_nwc_response + NwcRequestEvent.KIND -> Res.string.kind_nwc_request + NwcResponseEvent.KIND -> Res.string.kind_nwc_response LnZapPrivateEvent.KIND -> Res.string.kind_private_zaps - LnZapRequestEvent.KIND -> Res.string.kind_zap_req - LongTextNoteEvent.KIND -> Res.string.kind_blogs + ZapRequestEvent.KIND -> Res.string.kind_zap_req + LongFormContentEvent.KIND -> Res.string.kind_blogs MeetingRoomEvent.KIND -> Res.string.kind_meeting_room MeetingRoomPresenceEvent.KIND -> Res.string.kind_room_presence MeetingSpaceEvent.KIND -> Res.string.kind_meeting_space @@ -445,14 +445,14 @@ fun kindDisplayName(kind: Int): StringResource? = NNSEvent.KIND -> Res.string.kind_nns NipTextEvent.KIND -> Res.string.kind_nip NostrConnectEvent.KIND -> Res.string.kind_nostr_connect - NIP90StatusEvent.KIND -> Res.string.kind_dvm_status - NIP90ContentDiscoveryRequestEvent.KIND -> Res.string.kind_dvm_content_req - NIP90ContentDiscoveryResponseEvent.KIND -> Res.string.kind_dvm_content_resp - NIP90UserDiscoveryRequestEvent.KIND -> Res.string.kind_dvm_user_req - NIP90UserDiscoveryResponseEvent.KIND -> Res.string.kind_dvm_user_resp + DvmStatusEvent.KIND -> Res.string.kind_dvm_status + DvmContentDiscoveryRequestEvent.KIND -> Res.string.kind_dvm_content_req + DvmContentDiscoveryResponseEvent.KIND -> Res.string.kind_dvm_content_resp + DvmUserDiscoveryRequestEvent.KIND -> Res.string.kind_dvm_user_req + DvmUserDiscoveryResponseEvent.KIND -> Res.string.kind_dvm_user_resp OtsEvent.KIND -> Res.string.kind_ots PaymentTargetsEvent.KIND -> Res.string.kind_pay_to - PeopleListEvent.KIND -> Res.string.kind_people_lists + FollowSetEvent.KIND -> Res.string.kind_people_lists ProfileBadgesEvent.KIND -> Res.string.kind_profile_badges PictureEvent.KIND -> Res.string.kind_pictures WorkoutRecordEvent.KIND -> Res.string.kind_workouts @@ -460,12 +460,12 @@ fun kindDisplayName(kind: Int): StringResource? = ZapPollEvent.KIND -> Res.string.kind_zap_poll PollEvent.KIND -> Res.string.kind_poll PollResponseEvent.KIND -> Res.string.kind_poll_response - PrivateDmEvent.KIND -> Res.string.kind_nip04_dms + EncryptedDmEvent.KIND -> Res.string.kind_nip04_dms PrivateOutboxRelayListEvent.KIND -> Res.string.kind_private_relays ProxyRelayListEvent.KIND -> Res.string.kind_proxy_relays PublicMessageEvent.KIND -> Res.string.kind_public_message ReactionEvent.KIND -> Res.string.kind_reactions - ContactCardEvent.KIND -> Res.string.kind_contact_card + UserAssertionEvent.KIND -> Res.string.kind_contact_card RelayAuthEvent.KIND -> Res.string.kind_relay_auth RelayDiscoveryEvent.KIND -> Res.string.kind_relay_discovery RelayMonitorEvent.KIND -> Res.string.kind_relay_monitor @@ -473,9 +473,9 @@ fun kindDisplayName(kind: Int): StringResource? = ReportEvent.KIND -> Res.string.kind_reports RepostEvent.KIND -> Res.string.kind_reposts RequestToVanishEvent.KIND -> Res.string.kind_user_delete - SealedRumorEvent.KIND -> Res.string.kind_seals + SealEvent.KIND -> Res.string.kind_seals SearchRelayListEvent.KIND -> Res.string.kind_search_relays - StatusEvent.KIND -> Res.string.kind_user_status + UserStatusEvent.KIND -> Res.string.kind_user_status TextNoteEvent.KIND -> Res.string.kind_notes TextNoteModificationEvent.KIND -> Res.string.kind_edits TorrentEvent.KIND -> Res.string.kind_torrents @@ -485,15 +485,15 @@ fun kindDisplayName(kind: Int): StringResource? = VideoCurationSetEvent.KIND -> Res.string.kind_video_list VideoCollaborationEvent.KIND -> Res.string.kind_video_collaboration TextTrackEvent.KIND -> Res.string.kind_video_subtitles - VideoHorizontalEvent.KIND -> Res.string.kind_video_repl - VideoVerticalEvent.KIND -> Res.string.kind_shorts_repl + AddressableNormalVideoEvent.KIND -> Res.string.kind_video_repl + AddressableShortVideoEvent.KIND -> Res.string.kind_shorts_repl VideoNormalEvent.KIND -> Res.string.kind_video VideoShortEvent.KIND -> Res.string.kind_shorts VoiceEvent.KIND -> Res.string.kind_voice_msg VoiceReplyEvent.KIND -> Res.string.kind_voice_reply WakeUpEvent.KIND -> Res.string.kind_wake WebBookmarkEvent.KIND -> Res.string.kind_web_bookmark - WikiNoteEvent.KIND -> Res.string.kind_wiki + WikiArticleEvent.KIND -> Res.string.kind_wiki else -> null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt index a49752d925..4cfcdbed55 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt @@ -198,7 +198,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent @@ -478,7 +478,7 @@ val posts = setOf(0, 1, 6, 7, 16, 30023) val settings = setOf(3, 10002, 10000, 10001, 10003, 10004, 30000) val dms = setOf(4, GiftWrapEvent.KIND, EphemeralGiftWrapEvent.KIND, 10050) 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, DeletionRequestEvent.KIND, RequestToVanishEvent.KIND) @Composable fun KindChip(kind: Int) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt index 0d2c1399bf..d0f5ac593c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.commons.resources.dms import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent @@ -45,7 +45,7 @@ class DMRelayListViewModel : BasicRelaySetupInfoModel() { label = Res.string.dms, filter = Filter( - kinds = listOf(GiftWrapEvent.KIND, EphemeralGiftWrapEvent.KIND, PrivateDmEvent.KIND), + kinds = listOf(GiftWrapEvent.KIND, EphemeralGiftWrapEvent.KIND, EncryptedDmEvent.KIND), tags = mapOf("p" to listOf(account.pubKey)), ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsTopBar.kt index 86cb3bf712..0d129f2eec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/ShortsTopBar.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar import com.vitorpamplona.amethyst.ui.screen.FeedDefinition import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent @Composable fun ShortsTopBar( @@ -49,7 +49,7 @@ fun ShortsTopBar( // The feed's own kind window, plus whatever the list spinner narrowed it to — a hashtag // or a geohash says itself as a token; a follow set does not, and seeds nothing. val me = accountViewModel.userProfile().pubkeyHex - val seed = remember(list, me) { SearchSeed.merge(SearchSeed.ofKinds(VideoVerticalEvent.KIND), list.asSearchQuery(me)) } + val seed = remember(list, me) { SearchSeed.merge(SearchSeed.ofKinds(AddressableShortVideoEvent.KIND), list.asSearchQuery(me)) } UserDrawerSearchTopBar(accountViewModel, nav, seed) { ShortsTopNavFilterBar( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/dal/ShortsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/dal/ShortsFeedFilter.kt index 4e5390209a..dfbaf15534 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/dal/ShortsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/dal/ShortsFeedFilter.kt @@ -33,9 +33,9 @@ import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal.SupportedContent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoMeta import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent class ShortsFeedFilter( val account: Account, @@ -60,7 +60,7 @@ class ShortsFeedFilter( LocalCache.notes.filterIntoSet { _, it -> acceptableEvent(it, params) } + - LocalCache.addressables.filterIntoSet(VideoVerticalEvent.KIND) { _, it -> + LocalCache.addressables.filterIntoSet(AddressableShortVideoEvent.KIND) { _, it -> acceptableEvent(it, params) } return sort(notes) @@ -91,7 +91,7 @@ class ShortsFeedFilter( fun acceptableVideoiMetas(iMetas: List<VideoMeta>): Boolean = iMetas.any { videoFeedSupport.acceptableUrl(it.url, it.mimeType) } - fun acceptanceEvent(noteEvent: VideoVerticalEvent) = acceptableVideoiMetas(noteEvent.imetaTags()) + fun acceptanceEvent(noteEvent: AddressableShortVideoEvent) = acceptableVideoiMetas(noteEvent.imetaTags()) fun acceptanceEvent(noteEvent: VideoShortEvent) = acceptableVideoiMetas(noteEvent.imetaTags()) @@ -106,7 +106,7 @@ class ShortsFeedFilter( } return ( - (noteEvent is VideoVerticalEvent && acceptanceEvent(noteEvent)) || + (noteEvent is AddressableShortVideoEvent && acceptanceEvent(noteEvent)) || (noteEvent is VideoShortEvent && acceptanceEvent(noteEvent)) ) && params.match(noteEvent, note.relays) && diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index fbd05a0120..74d6155773 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -315,14 +315,14 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip13Pow.strongPoWOrNull import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent @@ -343,13 +343,13 @@ import com.vitorpamplona.quartz.nip43RelayMembers.list.RelayMembershipListEvent import com.vitorpamplona.quartz.nip43RelayMembers.removeMember.RelayRemoveMemberEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent @@ -358,8 +358,8 @@ import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip57Zaps.splits.hasZapSplitSetup import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent @@ -372,7 +372,7 @@ import com.vitorpamplona.quartz.nip71Video.VideoEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress import com.vitorpamplona.quartz.nip72ModCommunities.isACommunityPost -import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent +import com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent @@ -382,7 +382,7 @@ import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEven import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent import com.vitorpamplona.quartz.nipA4PublicMessages.PublicMessageEvent @@ -737,7 +737,7 @@ private fun FullBleedNoteCompose( // Zap receipts are signed by the recipient's lightning provider; show the // sender from the embedded zap request instead of the service key. val zapSender = - if (noteEvent is LnZapEvent) { + if (noteEvent is ZapReceiptEvent) { observeZapSender(baseNote, accountViewModel).value } else { null @@ -859,9 +859,9 @@ private fun FullBleedNoteCompose( when (noteEvent) { is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote, accountViewModel) - is LongTextNoteEvent -> RenderLongFormHeaderForThread(noteEvent, baseNote, accountViewModel) + is LongFormContentEvent -> RenderLongFormHeaderForThread(noteEvent, baseNote, accountViewModel) is ThreadEvent -> RenderThreadHeaderForThread(noteEvent) - is WikiNoteEvent -> RenderWikiHeaderForThread(noteEvent, accountViewModel, nav) + is WikiArticleEvent -> RenderWikiHeaderForThread(noteEvent, accountViewModel, nav) is ClassifiedsEvent -> RenderClassifiedsReaderForThread(noteEvent, baseNote, accountViewModel, nav) is CodeSnippetEvent -> RenderCodeSnippetHeaderForThread(noteEvent) } @@ -896,13 +896,13 @@ private fun FullBleedNoteCompose( WorkoutDisplay(baseNote, backgroundColor, canPreview = true, quotesLeft = 3, accountViewModel = accountViewModel, nav = nav) } else if (noteEvent is BaseVoiceEvent) { VoiceHeader(noteEvent, baseNote, accountViewModel, nav) - } else if (noteEvent is FileHeaderEvent) { + } else if (noteEvent is FileMetadataEvent) { FileHeaderDisplay(baseNote, roundedCorner = true, ContentScale.FillWidth, accountViewModel = accountViewModel) } else if (noteEvent is FileStorageHeaderEvent) { FileStorageHeaderDisplay(baseNote, roundedCorner = true, ContentScale.FillWidth, accountViewModel = accountViewModel) - } else if (noteEvent is PeopleListEvent) { + } else if (noteEvent is FollowSetEvent) { DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav) - } else if (noteEvent is FollowListEvent) { + } else if (noteEvent is StarterPackEvent) { DisplayFollowList(baseNote, false, accountViewModel, nav) } else if (noteEvent is ContactListEvent) { DisplayContactList(baseNote, accountViewModel, nav) @@ -954,7 +954,7 @@ private fun FullBleedNoteCompose( accountViewModel, nav, ) - } else if (noteEvent is ChatMessageRelayListEvent) { + } else if (noteEvent is DmRelayListEvent) { DisplayDMRelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is ChatMessageEncryptedFileHeaderEvent) { RenderChatMessageEncryptedFile( @@ -977,7 +977,7 @@ private fun FullBleedNoteCompose( RenderAttestorProficiency(baseNote, 3, backgroundColor, accountViewModel, nav) } else if (noteEvent is AdvertisedRelayListEvent) { DisplayNIP65RelayList(baseNote, backgroundColor, accountViewModel, nav) - } else if (noteEvent is LnZapEvent) { + } else if (noteEvent is ZapReceiptEvent) { RenderLnZap(baseNote, quotesLeft = 3, backgroundColor = backgroundColor, accountViewModel = accountViewModel, nav = nav) } else if (noteEvent is NutzapEvent) { RenderNutzap(baseNote, quotesLeft = 3, backgroundColor = backgroundColor, accountViewModel = accountViewModel, nav = nav) @@ -995,7 +995,7 @@ private fun FullBleedNoteCompose( DisplayProxyRelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is TrustedRelayListEvent) { DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav) - } else if (noteEvent is RelayFeedsListEvent) { + } else if (noteEvent is FavoriteRelayListEvent) { DisplayRelayFeedsList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is IndexerRelayListEvent) { DisplayIndexerRelayList(baseNote, backgroundColor, accountViewModel, nav) @@ -1070,7 +1070,7 @@ private fun FullBleedNoteCompose( RenderCalendarTimeSlotEvent(baseNote, accountViewModel, nav) } else if (noteEvent is CalendarDateSlotEvent) { RenderCalendarDateSlotEvent(baseNote, accountViewModel, nav) - } else if (noteEvent is GoalEvent) { + } else if (noteEvent is ZapGoalEvent) { RenderGoal(baseNote, accountViewModel, nav) } else if (noteEvent is FundraiserEvent) { RenderFundraiser(baseNote, makeItShort = false, accountViewModel, nav) @@ -1158,7 +1158,7 @@ private fun FullBleedNoteCompose( accountViewModel = accountViewModel, nav = nav, ) - } else if (noteEvent is PrivateDmEvent) { + } else if (noteEvent is EncryptedDmEvent) { RenderPrivateMessage( baseNote, false, @@ -1492,7 +1492,7 @@ private fun RenderClassifiedsReaderForThread( @Composable private fun RenderLongFormHeaderForThread( - noteEvent: LongTextNoteEvent, + noteEvent: LongFormContentEvent, note: Note, accountViewModel: AccountViewModel, ) { @@ -1558,7 +1558,7 @@ private fun RenderThreadHeaderForThread(noteEvent: ThreadEvent) { @Preview @Composable private fun RenderWikiHeaderForThreadPreview() { - val event = Event.fromJson("{\"id\":\"277f982a4cd3f67cc47ad9282176acabee1713848f547d6021e0c155572078e1\",\"pubkey\":\"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c\",\"created_at\":1708695717,\"kind\":30818,\"tags\":[[\"d\",\"amethyst\"],[\"a\",\"30818:f03e7c5262648e0b7823dfb49f8f17309cfec9cb14711413dcabdf3d7fc6369a:amethyst\",\"wss://relay.nostr.band\",\"fork\"],[\"e\",\"ceabc60c8022c472c727aa25ae7691885964366386ce265c47e5a78be6cb00be\",\"wss://relay.nostr.band\",\"fork\"],[\"title\",\"Amethyst\"],[\"published_at\",\"1708707133\"]],\"content\":\"An Android-only app written in Kotlin with support for over 90 event kinds. \\n\\n![](https://play-lh.googleusercontent.com/lvZlAm9dBrpHeOo7sIPKCsiKOLYLhR2b0FiOT4tyiwWO2dvsR2gDS0xk9tOOr9U-6uM=w240-h480-rw)\\n\",\"sig\":\"6748126a909a20dbdb67947a09d64e41d7140a79335a4ad675c6173d7dd5dbcab9c360dec617bd67bbbc20dfad416b15056eda2e20716cd6c425a84301a125a0\"}") as WikiNoteEvent + val event = Event.fromJson("{\"id\":\"277f982a4cd3f67cc47ad9282176acabee1713848f547d6021e0c155572078e1\",\"pubkey\":\"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c\",\"created_at\":1708695717,\"kind\":30818,\"tags\":[[\"d\",\"amethyst\"],[\"a\",\"30818:f03e7c5262648e0b7823dfb49f8f17309cfec9cb14711413dcabdf3d7fc6369a:amethyst\",\"wss://relay.nostr.band\",\"fork\"],[\"e\",\"ceabc60c8022c472c727aa25ae7691885964366386ce265c47e5a78be6cb00be\",\"wss://relay.nostr.band\",\"fork\"],[\"title\",\"Amethyst\"],[\"published_at\",\"1708707133\"]],\"content\":\"An Android-only app written in Kotlin with support for over 90 event kinds. \\n\\n![](https://play-lh.googleusercontent.com/lvZlAm9dBrpHeOo7sIPKCsiKOLYLhR2b0FiOT4tyiwWO2dvsR2gDS0xk9tOOr9U-6uM=w240-h480-rw)\\n\",\"sig\":\"6748126a909a20dbdb67947a09d64e41d7140a79335a4ad675c6173d7dd5dbcab9c360dec617bd67bbbc20dfad416b15056eda2e20716cd6c425a84301a125a0\"}") as WikiArticleEvent val accountViewModel = mockAccountViewModel() val editState = @@ -1602,7 +1602,7 @@ private fun RenderWikiHeaderForThreadPreview() { @Composable private fun RenderWikiHeaderForThread( - noteEvent: WikiNoteEvent, + noteEvent: WikiArticleEvent, accountViewModel: AccountViewModel, nav: INav, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileHeaderCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileHeaderCardCompose.kt index 45f3eae01e..523cb12efd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileHeaderCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileHeaderCardCompose.kt @@ -52,7 +52,7 @@ import com.vitorpamplona.amethyst.ui.note.types.toMediaContent import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent @Composable fun FileHeaderCardCompose( @@ -60,7 +60,7 @@ fun FileHeaderCardCompose( accountViewModel: AccountViewModel, nav: INav, ) { - val event = (baseNote.event as? FileHeaderEvent) ?: return + val event = (baseNote.event as? FileMetadataEvent) ?: return val backgroundColor = remember { mutableStateOf(Color.Transparent) } Column( @@ -90,7 +90,7 @@ fun FileHeaderCardCompose( @Composable private fun FileHeaderCardImage( note: Note, - event: FileHeaderEvent, + event: FileMetadataEvent, backgroundColor: MutableState<Color>, accountViewModel: AccountViewModel, ) { @@ -145,7 +145,7 @@ private fun FileHeaderCardImage( } @Composable -internal fun FileHeaderCardCaption(videoEvent: FileHeaderEvent) { +internal fun FileHeaderCardCaption(videoEvent: FileMetadataEvent) { val event = (videoEvent as? Event) ?: return val title = videoEvent.summary() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/StoriesTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/StoriesTopBar.kt index 74e418893f..831ee5d806 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/StoriesTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/StoriesTopBar.kt @@ -36,8 +36,8 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar import com.vitorpamplona.amethyst.ui.screen.FeedDefinition import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent @Composable fun StoriesTopBar( @@ -50,7 +50,7 @@ fun StoriesTopBar( // The feed's own kind window, plus whatever the list spinner narrowed it to — a hashtag // or a geohash says itself as a token; a follow set does not, and seeds nothing. val me = accountViewModel.userProfile().pubkeyHex - val seed = remember(list, me) { SearchSeed.merge(SearchSeed.ofKinds(VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND), list.asSearchQuery(me)) } + val seed = remember(list, me) { SearchSeed.merge(SearchSeed.ofKinds(AddressableNormalVideoEvent.KIND, AddressableShortVideoEvent.KIND), list.asSearchQuery(me)) } UserDrawerSearchTopBar(accountViewModel, nav, seed) { TopNavFilterBar( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt index a4f8fccfcd..d9b3fe531b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt @@ -53,7 +53,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.VideoCardCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilterAssemblerSubscription import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoEvent -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent @Composable fun VideoScreen( @@ -199,7 +199,7 @@ fun VideoFeedLoaded( Spacer(modifier = Modifier.height(8.dp)) } - item.event is FileHeaderEvent -> { + item.event is FileMetadataEvent -> { FileHeaderCardCompose(item, accountViewModel, nav) HorizontalDivider( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt index b9b67e2ffc..7fad0d489e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt @@ -34,12 +34,12 @@ import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip68Picture.PictureMeta -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoMeta import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent class VideoFeedFilter( val account: Account, @@ -72,10 +72,10 @@ class VideoFeedFilter( LocalCache.notes.filterIntoSet { _, it -> acceptableEvent(it, params) } + - LocalCache.addressables.filterIntoSet(VideoHorizontalEvent.KIND) { _, it -> + LocalCache.addressables.filterIntoSet(AddressableNormalVideoEvent.KIND) { _, it -> acceptableEvent(it, params) } + - LocalCache.addressables.filterIntoSet(VideoVerticalEvent.KIND) { _, it -> + LocalCache.addressables.filterIntoSet(AddressableShortVideoEvent.KIND) { _, it -> acceptableEvent(it, params) } @@ -101,11 +101,11 @@ class VideoFeedFilter( fun acceptanceEvent(noteEvent: PictureEvent) = acceptablePictureiMetas(noteEvent.imetaTags()) - fun acceptanceEvent(noteEvent: FileHeaderEvent) = acceptableUrls(noteEvent.urls(), noteEvent.mimeType()) + fun acceptanceEvent(noteEvent: FileMetadataEvent) = acceptableUrls(noteEvent.urls(), noteEvent.mimeType()) - fun acceptanceEvent(noteEvent: VideoVerticalEvent) = acceptableVideoiMetas(noteEvent.imetaTags()) + fun acceptanceEvent(noteEvent: AddressableShortVideoEvent) = acceptableVideoiMetas(noteEvent.imetaTags()) - fun acceptanceEvent(noteEvent: VideoHorizontalEvent) = acceptableVideoiMetas(noteEvent.imetaTags()) + fun acceptanceEvent(noteEvent: AddressableNormalVideoEvent) = acceptableVideoiMetas(noteEvent.imetaTags()) fun acceptanceEvent(noteEvent: VideoNormalEvent) = acceptableVideoiMetas(noteEvent.imetaTags()) @@ -122,9 +122,9 @@ class VideoFeedFilter( } return ( - (noteEvent is FileHeaderEvent && acceptanceEvent(noteEvent)) || - (noteEvent is VideoVerticalEvent && acceptanceEvent(noteEvent)) || - (noteEvent is VideoHorizontalEvent && acceptanceEvent(noteEvent)) || + (noteEvent is FileMetadataEvent && acceptanceEvent(noteEvent)) || + (noteEvent is AddressableShortVideoEvent && acceptanceEvent(noteEvent)) || + (noteEvent is AddressableNormalVideoEvent && acceptanceEvent(noteEvent)) || (noteEvent is VideoNormalEvent && acceptanceEvent(noteEvent)) || (noteEvent is VideoShortEvent && acceptanceEvent(noteEvent)) || (noteEvent is PictureEvent && acceptanceEvent(noteEvent)) diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/appfunctions/AmethystAppFunctions.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/appfunctions/AmethystAppFunctions.kt index a0dd8d1bf9..f14ec732b7 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/appfunctions/AmethystAppFunctions.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/appfunctions/AmethystAppFunctions.kt @@ -54,14 +54,14 @@ import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey import com.vitorpamplona.quartz.nip19Bech32.entities.NPub -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcErrorResponse import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceErrorResponse import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -778,7 +778,7 @@ class AmethystAppFunctions { val filter = Filter( - kinds = listOf(LnZapEvent.KIND, Bolt12ZapEvent.KIND), + kinds = listOf(ZapReceiptEvent.KIND, Bolt12ZapEvent.KIND), tags = mapOf("p" to listOf(myPub)), since = sinceSecs, limit = 500, @@ -789,7 +789,7 @@ class AmethystAppFunctions { idleTimeoutMs = GEMINI_FETCH_TIMEOUT_MS, ) - val receipts = events.mapNotNull { it as? LnZapEvent } + val receipts = events.mapNotNull { it as? ZapReceiptEvent } val zapperIds = mutableSetOf<HexKey>() var totalSats = 0L var unparseable = 0 @@ -936,7 +936,7 @@ class AmethystAppFunctions { val filter = SearchActions.searchNotesFilter( query = query, - kinds = listOf(LongTextNoteEvent.KIND), + kinds = listOf(LongFormContentEvent.KIND), limit = cappedLimit, ) ?: return SearchNotesResult.empty() @@ -953,7 +953,7 @@ class AmethystAppFunctions { val hits = events - .mapNotNull { it as? LongTextNoteEvent } + .mapNotNull { it as? LongFormContentEvent } .take(cappedLimit) .map { (it as com.vitorpamplona.quartz.nip01Core.core.Event).toFeedNoteHit() } @@ -1403,7 +1403,7 @@ class AmethystAppFunctions { amountMillisats = ZapActions.satsToMillisats(sats), inboxRelays = account.nip65RelayList.inboxFlow.value, comment = comment, - zapType = LnZapEvent.ZapType.PUBLIC, + zapType = ZapReceiptEvent.ZapType.PUBLIC, ) val invoice = fetchInvoiceOrThrow(lnAddress, sats, comment, zapRequest) @@ -1614,7 +1614,7 @@ class AmethystAppFunctions { lookupLnAddress = lookupLnAddress, lookupInboxRelays = lookupInboxRelays, comment = trimmedComment, - zapType = LnZapEvent.ZapType.PUBLIC, + zapType = ZapReceiptEvent.ZapType.PUBLIC, ) if (requests.isEmpty()) { throw AppFunctionInvalidArgumentException( @@ -1787,7 +1787,7 @@ class AmethystAppFunctions { lnAddress: String, sats: Long, comment: String, - zapRequest: com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent, + zapRequest: com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent, ): String { // Compute the LNURL-pay endpoint so we can ask the privacy-aware // HttpClient builder for the right OkHttpClient for that host. diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelFlowTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelFlowTest.kt index e504817753..aaf1c26d9b 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelFlowTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelFlowTest.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsViewMode import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.dal.CalendarAppointmentsFeedFilter import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -90,7 +90,7 @@ class CalendarsViewModelFlowTest { dTag: String?, title: String, members: List<Address> = emptyList(), - ) = CalendarEvent( + ) = CalendarCollectionEvent( id = id, pubKey = pubKey, createdAt = 1_700_000_000L, @@ -121,7 +121,7 @@ class CalendarsViewModelFlowTest { * for the length of the test: `LocalCache.addressables` is a weak cache, and with nothing * holding a reference a calendar can be collected out from under the assertions. */ - private fun consume(vararg calendars: CalendarEvent): List<AddressableNote?> { + private fun consume(vararg calendars: CalendarCollectionEvent): List<AddressableNote?> { calendars.forEach { LocalCache.justConsumeMyOwnEvent(it) } return calendars.map { LocalCache.getAddressableNoteIfExists(it.address()) } } diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/AddressedAuthorRoutingTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/AddressedAuthorRoutingTest.kt index 815b6e933b..3033cec228 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/AddressedAuthorRoutingTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/AddressedAuthorRoutingTest.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.model -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import com.vitorpamplona.quartz.nip52Calendar.rsvp.CalendarRSVPEvent import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -49,7 +49,7 @@ class AddressedAuthorRoutingTest { ) private fun calendar(vararg tags: Array<String>) = - CalendarEvent( + CalendarCollectionEvent( id = "22".repeat(32), pubKey = host, createdAt = 1700000000, diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/BuzzWorkspaceChannelTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/BuzzWorkspaceChannelTest.kt index 30766d0f2c..97512fd09b 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/BuzzWorkspaceChannelTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/BuzzWorkspaceChannelTest.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.buzz.stream.StreamMessageV2Event import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip29RelayGroups.GroupId import io.mockk.every import io.mockk.mockk @@ -225,7 +225,7 @@ class BuzzWorkspaceChannelTest { // The author deletes their own edit (NIP-09). It must stop overlaying the message and // be unlinked from Note.edits, not linger as a stale overlay. - val deletion = signer.sign(DeletionEvent.build(listOf(edit))) + val deletion = signer.sign(DeletionRequestEvent.build(listOf(edit))) LocalCache.checkDeletionAndConsume(deletion, buzzRelay, false) assertNull("a deleted edit must no longer overlay its message", target.latestBuzzEdit()) diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/HomeFeedTypeTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/HomeFeedTypeTest.kt index e9580d0ac5..7b96f5a90d 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/HomeFeedTypeTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/HomeFeedTypeTest.kt @@ -24,10 +24,10 @@ import com.vitorpamplona.amethyst.commons.model.HomeFeedType import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull @@ -76,8 +76,8 @@ class HomeFeedTypeTest { fun picturesVideosShortsAndTorrentsOwnTheirKinds() { assertEquals(listOf(PictureEvent.KIND), HomeFeedType.PICTURES.kinds) // Long-form videos are the horizontal/normal kinds; vertical/short kinds belong to Shorts. - assertEquals(listOf(VideoNormalEvent.KIND, VideoHorizontalEvent.KIND), HomeFeedType.VIDEOS.kinds) - assertEquals(listOf(VideoShortEvent.KIND, VideoVerticalEvent.KIND), HomeFeedType.SHORTS.kinds) + assertEquals(listOf(VideoNormalEvent.KIND, AddressableNormalVideoEvent.KIND), HomeFeedType.VIDEOS.kinds) + assertEquals(listOf(VideoShortEvent.KIND, AddressableShortVideoEvent.KIND), HomeFeedType.SHORTS.kinds) assertEquals(listOf(TorrentEvent.KIND), HomeFeedType.TORRENTS.kinds) } diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteAlgoFeeds/FavoriteAlgoFeedTopNavFilterTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteAlgoFeeds/FavoriteAlgoFeedTopNavFilterTest.kt index a8872f4ad3..3d06dfb3d5 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteAlgoFeeds/FavoriteAlgoFeedTopNavFilterTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/model/topNavFeeds/favoriteAlgoFeeds/FavoriteAlgoFeedTopNavFilterTest.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.model.topNavFeeds.favoriteAlgoFeeds import com.vitorpamplona.amethyst.commons.model.topNavFeeds.favoriteAlgoFeeds.FavoriteAlgoFeedTopNavFilter import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test @@ -34,7 +34,7 @@ class FavoriteAlgoFeedTopNavFilterTest { private fun longFormNote( pubkey: String, dTag: String, - ) = LongTextNoteEvent( + ) = LongFormContentEvent( id = "0".repeat(64), pubKey = pubkey, createdAt = 1, diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/okhttp/DelayingTestSigner.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/okhttp/DelayingTestSigner.kt index ff18cabafc..9cb566f031 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/okhttp/DelayingTestSigner.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/okhttp/DelayingTestSigner.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import kotlinx.coroutines.delay /** @@ -74,7 +74,7 @@ class DelayingTestSigner( fromPublicKey: HexKey, ) = unsupported() - override suspend fun decryptZapEvent(event: LnZapRequestEvent) = unsupported() + override suspend fun decryptZapEvent(event: ZapRequestEvent) = unsupported() override suspend fun deriveKey(nonce: HexKey) = unsupported() diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterGroupNotificationsToPubkeyTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterGroupNotificationsToPubkeyTest.kt index 475a99e29b..d87a15fded 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterGroupNotificationsToPubkeyTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterGroupNotificationsToPubkeyTest.kt @@ -58,7 +58,7 @@ class FilterGroupNotificationsToPubkeyTest { assertTrue(GroupNotificationKinds.contains(9)) // ChatEvent assertTrue(GroupNotificationKinds.contains(1111)) // CommentEvent assertTrue(GroupNotificationKinds.contains(7)) // ReactionEvent - assertTrue(GroupNotificationKinds.contains(9735)) // LnZapEvent + assertTrue(GroupNotificationKinds.contains(9735)) // ZapReceiptEvent } @Test diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/resourceusage/ResourceUsageLedgerTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/resourceusage/ResourceUsageLedgerTest.kt index 9eca6b4480..63bbe4a78b 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/resourceusage/ResourceUsageLedgerTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/resourceusage/ResourceUsageLedgerTest.kt @@ -44,7 +44,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.CoroutineScope @@ -650,7 +650,7 @@ class MeteringNostrSignerTest { fromPublicKey: HexKey, ) = "dec44" - override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent = throw UnsupportedOperationException("fake") + override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = throw UnsupportedOperationException("fake") override suspend fun deriveKey(nonce: HexKey): HexKey = "bb".repeat(32) diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsVideoEventBuilderTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsVideoEventBuilderTest.kt index 1d7223b49c..114660dbb0 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsVideoEventBuilderTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsVideoEventBuilderTest.kt @@ -25,8 +25,8 @@ import com.davotoula.lightcompressor.hls.HlsRenditionSummary import com.davotoula.lightcompressor.hls.HlsUploaded import com.davotoula.lightcompressor.hls.Rendition import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull @@ -122,7 +122,7 @@ class HlsVideoEventBuilderTest { assertTrue("expected Horizontal template", result.template is HlsVideoEventTemplate.Horizontal) val template = (result.template as HlsVideoEventTemplate.Horizontal).template - assertEquals(VideoHorizontalEvent.KIND, template.kind) + assertEquals(AddressableNormalVideoEvent.KIND, template.kind) assertEquals("A cool video", template.content) } @@ -132,7 +132,7 @@ class HlsVideoEventBuilderTest { assertTrue("expected Vertical template", result.template is HlsVideoEventTemplate.Vertical) val template = (result.template as HlsVideoEventTemplate.Vertical).template - assertEquals(VideoVerticalEvent.KIND, template.kind) + assertEquals(AddressableShortVideoEvent.KIND, template.kind) } @Test diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/RouteForPointerTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/RouteForPointerTest.kt index 92337c0f2e..19964c6862 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/RouteForPointerTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/RouteForPointerTest.kt @@ -31,12 +31,12 @@ import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.utils.EventFactory import io.mockk.mockk import org.junit.Assert.assertEquals @@ -73,7 +73,7 @@ class RouteForPointerTest { // the wrap has to be opened before it can say anything assertNull(routeForPointer(GiftWrapEvent.KIND, id)) // addressables are cited by `a` tag, not by id - assertNull(routeForPointer(LongTextNoteEvent.KIND, id)) + assertNull(routeForPointer(LongFormContentEvent.KIND, id)) // a kind:0 IS the person — Route.Profile, not a note assertNull(routeForPointer(MetadataEvent.KIND, id)) } @@ -111,7 +111,7 @@ class RouteForPointerTest { */ @Test fun addressableVideoKindsDoNotTakeTheShortcut() { - assertNull(routeForPointer(VideoHorizontalEvent.KIND, id)) - assertNull(routeForPointer(VideoVerticalEvent.KIND, id)) + assertNull(routeForPointer(AddressableNormalVideoEvent.KIND, id)) + assertNull(routeForPointer(AddressableShortVideoEvent.KIND, id)) } } diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/QrPayloadTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/QrPayloadTest.kt index 72a8c43723..e00804aded 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/QrPayloadTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/QrPayloadTest.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -79,9 +79,9 @@ class QrPayloadTest { runTest { // build(description, title, summary, image, publishedAt, dTag, createdAt, init) // — `title` is a required positional; `dTag` must be named. - // LongTextNoteEvent.kt:148-157. + // LongFormContentEvent.kt:148-157. val event = - LongTextNoteEvent + LongFormContentEvent .build("body", "My Article", dTag = "my-article") {} .let { aliceSigner.sign(it) } val note = AddressableNote(event.address()) diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/SharedNoteCardTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/SharedNoteCardTest.kt index 0e70df1ace..7d76a5852f 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/SharedNoteCardTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/note/share/SharedNoteCardTest.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder import com.vitorpamplona.quartz.nip92IMeta.imetas import kotlinx.coroutines.test.runTest @@ -129,7 +129,7 @@ class SharedNoteCardTest { fun article_prefersItsTitle() = runTest { val note = - LongTextNoteEvent + LongFormContentEvent .build("the body", "My Article", dTag = "a1") {} .let { aliceSigner.sign(it) } diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationTagsAnEventByUserTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationTagsAnEventByUserTest.kt index 786c4eda28..f57bf851e3 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationTagsAnEventByUserTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationTagsAnEventByUserTest.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test @@ -81,10 +81,10 @@ class NotificationTagsAnEventByUserTest { 1000, arrayOf( arrayOf("E", zapId, "", wallet), - arrayOf("K", LnZapEvent.KIND.toString()), + arrayOf("K", ZapReceiptEvent.KIND.toString()), arrayOf("P", wallet), arrayOf("e", zapId, "", wallet), - arrayOf("k", LnZapEvent.KIND.toString()), + arrayOf("k", ZapReceiptEvent.KIND.toString()), arrayOf("p", wallet), arrayOf("p", me), ), @@ -109,10 +109,10 @@ class NotificationTagsAnEventByUserTest { 1000, arrayOf( arrayOf("E", zapId, "", wallet), - arrayOf("K", LnZapEvent.KIND.toString()), + arrayOf("K", ZapReceiptEvent.KIND.toString()), arrayOf("P", wallet), arrayOf("e", zapId, "", wallet), - arrayOf("k", LnZapEvent.KIND.toString()), + arrayOf("k", ZapReceiptEvent.KIND.toString()), arrayOf("p", wallet), ), "nice zap", diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt index 34431d9e3c..60e6b13cd7 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip17Dm.NIP17Factory import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import kotlinx.coroutines.runBlocking import org.junit.Assert @@ -78,7 +78,7 @@ class GiftWrapBenchmark { val event = it.unwrapThrowing(keyToUse) event.checkSignature() - if (event is SealedRumorEvent) { + if (event is SealEvent) { val innerData = event.unsealThrowing(keyToUse) Assert.assertEquals(message, innerData.content) } else { @@ -116,7 +116,7 @@ class GiftWrapBenchmark { val seal = wrap.unwrapThrowing(keyToUse) seal.checkSignature() - if (seal is SealedRumorEvent) { + if (seal is SealEvent) { val innerData = seal.unsealThrowing(keyToUse) Assert.assertEquals(message, innerData.content) } else { diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt index 62c3e676af..2501bb7845 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt @@ -37,7 +37,7 @@ import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip44Encryption.Nip44 import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import junit.framework.TestCase.assertNotNull import kotlinx.coroutines.runBlocking @@ -62,7 +62,7 @@ class GiftWrapReceivingBenchmark { runBlocking { GiftWrapEvent.create( event = - SealedRumorEvent.create( + SealEvent.create( event = sender.sign( ChatMessageEvent.build( @@ -84,7 +84,7 @@ class GiftWrapReceivingBenchmark { fun createSeal( sender: NostrSigner, receiver: NostrSigner, - ): SealedRumorEvent = + ): SealEvent = runBlocking { val msg = sender.sign( @@ -101,7 +101,7 @@ class GiftWrapReceivingBenchmark { }, ) - SealedRumorEvent.create( + SealEvent.create( event = msg, encryptTo = receiver.pubKey, signer = sender, diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapSigningBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapSigningBenchmark.kt index 1395ab7202..796f2c309e 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapSigningBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapSigningBenchmark.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip17Dm.messages.changeSubject import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import kotlinx.coroutines.runBlocking import org.junit.Rule @@ -95,7 +95,7 @@ class GiftWrapSigningBenchmark { benchmarkRule.measureRepeated { runBlocking { - SealedRumorEvent.create( + SealEvent.create( event = msg, encryptTo = receiver.pubKey, signer = sender, @@ -128,7 +128,7 @@ class GiftWrapSigningBenchmark { val seal = runBlocking { - SealedRumorEvent.create( + SealEvent.create( event = msg, encryptTo = receiver.pubKey, signer = sender, @@ -169,7 +169,7 @@ class GiftWrapSigningBenchmark { val seal = runBlocking { - SealedRumorEvent.create( + SealEvent.create( event = msg, encryptTo = receiver.pubKey, signer = sender, diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeDBInsertBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeDBInsertBenchmark.kt index 228f247b89..5882a26a03 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeDBInsertBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeDBInsertBenchmark.kt @@ -28,7 +28,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventStore -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip40Expiration.isExpired import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.flattenToSet @@ -72,7 +72,7 @@ class LargeDBInsertBenchmark : BaseLargeCacheBenchmark() { @Test fun bench40DeletionRequestsEvents() { val context = ApplicationProvider.getApplicationContext<Context>() - val deletions = allEvents.filterIsInstance<DeletionEvent>() + val deletions = allEvents.filterIsInstance<DeletionRequestEvent>() val deletionIds = deletions.map { it.deleteEventIds() }.flattenToSet() val deletionAddresses = deletions.map { it.deleteAddressIds() }.flattenToSet() diff --git a/cli/ROADMAP.md b/cli/ROADMAP.md index 09b80c2072..41f46bbc9c 100644 --- a/cli/ROADMAP.md +++ b/cli/ROADMAP.md @@ -71,7 +71,7 @@ Status legend: ✅ shipped · 📦 logic lives in `commons/`, needs a command · | NIP-5A nsites + NIP-5D napplets | ✅ | `NsiteCommands` + `NappletCommands` — fetch/publish/serve/list with sha256 + aggregate-hash verification and `requires` capability reporting. | | Podcasting 2.0 / podstr (`amy podcast20`) | ✅ | `Podcast20Commands` — kind:30078 metadata, 30054 episodes, 30055 trailers, list. | | Follows-of-follows (`amy fof get/list/sync`) | ✅ | `FofCommand` — single-hop social proof from the local store (`wot` kept as deprecation alias). | -| NIP-85 GrapeRank web-of-trust (`amy graperank`) | ✅ | `GrapeRankCommand` — outbox-model crawl + scoring engine in `commons/wot/` (`GrapeRank`, `TrustGraph`, `TrustGraphBuilder`); every score run persists kind:30382 `ContactCardEvent` cards to the local store (diffed against prior ranks, kind:5 retractions), `publish` mirrors that set to the operator relays via NIP-77 up-sync, `rank` reads cards back, plus `register` / `unregister` / `providers` for the kind:10040 `TrustProviderListEvent` discovery layer. | +| NIP-85 GrapeRank web-of-trust (`amy graperank`) | ✅ | `GrapeRankCommand` — outbox-model crawl + scoring engine in `commons/wot/` (`GrapeRank`, `TrustGraph`, `TrustGraphBuilder`); every score run persists kind:30382 `UserAssertionEvent` cards to the local store (diffed against prior ranks, kind:5 retractions), `publish` mirrors that set to the operator relays via NIP-77 up-sync, `rank` reads cards back, plus `register` / `unregister` / `providers` for the kind:10040 `TrustProviderListEvent` discovery layer. | | NIP-72 communities | 🆕 | | | NIP-78 app-specific data (settings sync) | 🆕 | | | Long-form (NIP-23) publish / read | 🆕 | | diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Context.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Context.kt index af23b2f575..1db3013b82 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Context.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Context.kt @@ -66,7 +66,7 @@ import com.vitorpamplona.quartz.nip01Core.store.verifyAndInsert import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip05DnsIdentifiers.resolveUserHexOrNull import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip46RemoteSigner.signer.NostrSignerRemote import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip66RelayMonitor.reachability.RelayObserver @@ -760,11 +760,11 @@ class Context( * for [pubKey], or `null` if Amy has never observed one. Used by * `dm send` to resolve where to deliver a wrap. */ - suspend fun dmInboxOf(pubKey: HexKey): ChatMessageRelayListEvent? = + suspend fun dmInboxOf(pubKey: HexKey): DmRelayListEvent? = store .query<Event>( - Filter(authors = listOf(pubKey), kinds = listOf(ChatMessageRelayListEvent.KIND), limit = 1), - ).firstOrNull() as? ChatMessageRelayListEvent + Filter(authors = listOf(pubKey), kinds = listOf(DmRelayListEvent.KIND), limit = 1), + ).firstOrNull() as? DmRelayListEvent /** * Latest known kind:10051 KeyPackage relay list (MIP-00) for diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GiftCommands.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GiftCommands.kt index 4df0d26172..73db57cb9d 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GiftCommands.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GiftCommands.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.cli.Context import com.vitorpamplona.amethyst.cli.DataDir import com.vitorpamplona.amethyst.cli.Output import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent /** @@ -41,7 +41,7 @@ import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent * Inner/wrap JSON comes from the positional argument or stdin (`-`). * * Thin assembly only: seal/wrap/unwrap all live in quartz - * (`SealedRumorEvent`, `GiftWrapEvent`). + * (`SealEvent`, `GiftWrapEvent`). */ object GiftCommands { val USAGE: String = @@ -87,7 +87,7 @@ object GiftCommands { Context.open(dataDir).use { ctx -> ctx.prepare() val peer = ctx.requireUserHex(to) - val seal = SealedRumorEvent.create(event = inner, encryptTo = peer, signer = ctx.signer) + val seal = SealEvent.create(event = inner, encryptTo = peer, signer = ctx.signer) val giftWrap = GiftWrapEvent.create(event = seal, recipientPubKey = peer) val wrapNode = Output.mapper.readTree(giftWrap.toJson()) @@ -134,7 +134,7 @@ object GiftCommands { return Output.error("decrypt_failed", "could not unwrap (is this gift addressed to the active account?): ${e.message}") } // The wrap holds a seal (kind:13); unseal it to recover the rumor. - val inner = if (unwrapped is SealedRumorEvent) unwrapped.unsealThrowing(ctx.signer) else unwrapped + val inner = if (unwrapped is SealEvent) unwrapped.unsealThrowing(ctx.signer) else unwrapped Output.emit(mapOf("event" to Output.mapper.readTree(inner.toJson()))) return 0 } diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GrapeRankCommand.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GrapeRankCommand.kt index 0d4ae9c3b8..3beb8aacc0 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GrapeRankCommand.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GrapeRankCommand.kt @@ -51,7 +51,7 @@ import com.vitorpamplona.amethyst.cli.commands.graperank.GrapeRankScore * Answers "do I need to crawl again?" with no network and no signing. * - `amy graperank score [OBSERVER]` — local only: build the graph from the store, * score (same as bare `--offline`), and ALWAYS reconcile the result into the - * store as kind:30382 ContactCardEvent cards signed by the observer's + * store as kind:30382 UserAssertionEvent cards signed by the observer's * per-observer service key (`rank = round(score*100)`, cutoff `--min-rank`): * changed ranks are re-signed, unchanged ones skipped, dropped targets * retracted with a kind:5. That persisted card set is what `publish` and diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/RelayCommands.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/RelayCommands.kt index 09f31e2e29..3d5a3746ec 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/RelayCommands.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/RelayCommands.kt @@ -32,14 +32,14 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrlOrNull import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayFacet import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent @@ -144,14 +144,14 @@ object RelayCommands { Flat( "dm", "dm", - ChatMessageRelayListEvent.KIND, + DmRelayListEvent.KIND, setOf("chat", "inbox-dm"), // allRelays(), not relays(): every Flat here is read for SELF, // and the filtered accessor drops local entries meant for // attacker-supplied lists — making a deliberately configured // local relay report as no configuration at all. read = { c, pk -> c.dmInboxOf(pk)?.allRelays().orEmpty() }, - build = { c, r -> ChatMessageRelayListEvent.create(r, c.signer) }, + build = { c, r -> DmRelayListEvent.create(r, c.signer) }, ), Flat( "key-package", @@ -220,10 +220,10 @@ object RelayCommands { Flat( "feeds", "feeds", - RelayFeedsListEvent.KIND, + FavoriteRelayListEvent.KIND, setOf("favorites", "relay_feeds"), - read = { c, pk -> (c.latestReplaceable(pk, RelayFeedsListEvent.KIND) as? RelayFeedsListEvent)?.decryptRelays(c.signer).orEmpty() }, - build = { c, r -> RelayFeedsListEvent.create(r, c.signer) }, + read = { c, pk -> (c.latestReplaceable(pk, FavoriteRelayListEvent.KIND) as? FavoriteRelayListEvent)?.decryptRelays(c.signer).orEmpty() }, + build = { c, r -> FavoriteRelayListEvent.create(r, c.signer) }, ), ) diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/RelayGroupCommands.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/RelayGroupCommands.kt index 2f7e08bfdf..0bf79a57d1 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/RelayGroupCommands.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/RelayGroupCommands.kt @@ -33,9 +33,9 @@ import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip29RelayGroups.hTag import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateGroupEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.EditMetadataEvent -import com.vitorpamplona.quartz.nip29RelayGroups.request.JoinRequestEvent -import com.vitorpamplona.quartz.nip29RelayGroups.request.LeaveRequestEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupEditMetadataEvent +import com.vitorpamplona.quartz.nip29RelayGroups.request.GroupJoinRequestEvent +import com.vitorpamplona.quartz.nip29RelayGroups.request.GroupLeaveRequestEvent import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.GroupTag import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent import com.vitorpamplona.quartz.nipC7Chats.ChatEvent @@ -116,7 +116,7 @@ object RelayGroupCommands { val createAck = ctx.publish(ctx.signer.sign(CreateGroupEvent.build(groupId)), target) val status = groupStatus(isPrivate, isClosed) - val edit = EditMetadataEvent.build(groupId, name = name, about = about, status = status) + val edit = GroupEditMetadataEvent.build(groupId, name = name, about = about, status = status) val editAck = ctx.publish(ctx.signer.sign(edit), target) // Track it in our own kind:10009 so `relaygroup list` shows it, matching // the Android create flow (Account.createRelayGroup → follow). @@ -154,7 +154,7 @@ object RelayGroupCommands { Context.open(dataDir).use { ctx -> ctx.prepare() - val join = JoinRequestEvent.build(groupId, reason = args.flag("reason") ?: "", inviteCode = args.flag("code")) + val join = GroupJoinRequestEvent.build(groupId, reason = args.flag("reason") ?: "", inviteCode = args.flag("code")) args.rejectUnknown() val signed = ctx.signer.sign(join) val ack = ctx.publish(signed, setOf(relay)) @@ -184,7 +184,7 @@ object RelayGroupCommands { Context.open(dataDir).use { ctx -> ctx.prepare() - val signed = ctx.signer.sign(LeaveRequestEvent.build(groupId)) + val signed = ctx.signer.sign(GroupLeaveRequestEvent.build(groupId)) val ack = ctx.publish(signed, setOf(relay)) RawEventSupport.publishGuard(ack, signed.id)?.let { return it } val listed = updateGroupList(ctx, relay, groupId, add = false) diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/RelayGroupModerationCommands.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/RelayGroupModerationCommands.kt index 12bde96071..937b14ed19 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/RelayGroupModerationCommands.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/RelayGroupModerationCommands.kt @@ -26,10 +26,10 @@ import com.vitorpamplona.amethyst.cli.DataDir import com.vitorpamplona.amethyst.cli.Output import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateInviteEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.EditMetadataEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.PutUserEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.RemoveUserEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupCreateInviteEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupEditMetadataEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupPutUserEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupRemoveUserEvent /** * Moderator/admin write verbs for a relay group (the relay is the final authority @@ -94,7 +94,7 @@ object RelayGroupModerationCommands { // A kind-9002 re-asserts the whole metadata, so omitted fields must be carried over // from the current 39000 — otherwise editing only a flag would wipe name/about/tags. val template = - EditMetadataEvent.build( + GroupEditMetadataEvent.build( groupId, name = args.flag("name") ?: meta?.name(), about = args.flag("about") ?: meta?.about(), @@ -127,7 +127,7 @@ object RelayGroupModerationCommands { ): Int { val code = Args(rest).flag("code") ?: return Output.error("bad_args", "relaygroup invite RELAY GROUP_ID --code CODE") return publishScoped(dataDir, rest, "relaygroup invite RELAY GROUP_ID --code CODE", allowedFlags = arrayOf("code")) { _, groupId, _ -> - CreateInviteEvent.build(groupId, code) + GroupCreateInviteEvent.build(groupId, code) } } @@ -153,7 +153,7 @@ object RelayGroupModerationCommands { Context.open(dataDir).use { ctx -> ctx.prepare() val pubkey = ctx.requireUserHex(user) - val signed = ctx.signer.sign(PutUserEvent.build(groupId, listOf(pubkey to roles))) + val signed = ctx.signer.sign(GroupPutUserEvent.build(groupId, listOf(pubkey to roles))) val ack = ctx.publish(signed, setOf(relay)) RawEventSupport.publishGuard(ack, signed.id)?.let { return it } Output.emit( @@ -186,7 +186,7 @@ object RelayGroupModerationCommands { Context.open(dataDir).use { ctx -> ctx.prepare() val pubkey = ctx.requireUserHex(user) - val signed = ctx.signer.sign(RemoveUserEvent.build(groupId, listOf(pubkey))) + val signed = ctx.signer.sign(GroupRemoveUserEvent.build(groupId, listOf(pubkey))) val ack = ctx.publish(signed, setOf(relay)) RawEventSupport.publishGuard(ack, signed.id)?.let { return it } Output.emit( diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/StatusReport.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/StatusReport.kt index 5109d78728..91e692140d 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/StatusReport.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/StatusReport.kt @@ -34,7 +34,7 @@ import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.store.IEventStore import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip60Cashu.wallet.CashuWalletEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent @@ -317,7 +317,7 @@ internal object StatusReport { var profile: MetadataEvent? = null var contacts: ContactListEvent? = null var relayList: AdvertisedRelayListEvent? = null - var dmInbox: ChatMessageRelayListEvent? = null + var dmInbox: DmRelayListEvent? = null var wallet = false // Newest-first with limit 1 gives the last-activity stamp. @@ -330,7 +330,7 @@ internal object StatusReport { MetadataEvent.KIND, ContactListEvent.KIND, AdvertisedRelayListEvent.KIND, - ChatMessageRelayListEvent.KIND, + DmRelayListEvent.KIND, CashuWalletEvent.KIND, ) store.query<Event>(Filter(authors = authors, kinds = describes)) { event -> @@ -340,7 +340,7 @@ internal object StatusReport { is MetadataEvent -> if (isNewer(event, profile)) profile = event is ContactListEvent -> if (isNewer(event, contacts)) contacts = event is AdvertisedRelayListEvent -> if (isNewer(event, relayList)) relayList = event - is ChatMessageRelayListEvent -> if (isNewer(event, dmInbox)) dmInbox = event + is DmRelayListEvent -> if (isNewer(event, dmInbox)) dmInbox = event is CashuWalletEvent -> wallet = true else -> Unit } diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/ZapCommand.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/ZapCommand.kt index 7f0300c989..df39413b02 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/ZapCommand.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/ZapCommand.kt @@ -33,8 +33,8 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import okhttp3.OkHttpClient /** @@ -222,8 +222,8 @@ object ZapCommand { sats: Long, lnAddress: String, comment: String, - request: LnZapRequestEvent, - zapType: LnZapEvent.ZapType, + request: ZapRequestEvent, + zapType: ZapReceiptEvent.ZapType, timeoutMs: Long, settleWith: NDebit?, zappedEventId: HexKey? = null, @@ -300,7 +300,7 @@ object ZapCommand { sats: Long, comment: String, zappedEventId: HexKey, - zapType: LnZapEvent.ZapType, + zapType: ZapReceiptEvent.ZapType, requests: List<ZapActions.ZapRequestForSplit>, timeoutMs: Long, settleWith: NDebit?, @@ -350,11 +350,11 @@ object ZapCommand { ) } - private fun parseZapType(args: Args): LnZapEvent.ZapType = + private fun parseZapType(args: Args): ZapReceiptEvent.ZapType = when { - args.bool("anon") -> LnZapEvent.ZapType.ANONYMOUS - args.bool("private") -> LnZapEvent.ZapType.PRIVATE - else -> LnZapEvent.ZapType.PUBLIC + args.bool("anon") -> ZapReceiptEvent.ZapType.ANONYMOUS + args.bool("private") -> ZapReceiptEvent.ZapType.PRIVATE + else -> ZapReceiptEvent.ZapType.PUBLIC } private suspend fun fetchLatestMetadata( diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/graperank/GrapeRankOperator.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/graperank/GrapeRankOperator.kt index c380072c06..057768d857 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/graperank/GrapeRankOperator.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/graperank/GrapeRankOperator.kt @@ -34,7 +34,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent @@ -45,7 +45,7 @@ import com.vitorpamplona.quartz.nip85TrustedAssertions.list.serviceProviders import com.vitorpamplona.quartz.nip85TrustedAssertions.list.tags.ProviderTypes import com.vitorpamplona.quartz.nip85TrustedAssertions.list.tags.ServiceProviderTag import com.vitorpamplona.quartz.nip85TrustedAssertions.list.tags.ServiceType -import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent import com.vitorpamplona.quartz.utils.TimeUtils /** @@ -90,8 +90,8 @@ object GrapeRankOperator { linkedMapOf<String, Any?>( "observer" to observer, "provider_pubkey" to rec.providerPubKey, - "cards" to ctx.store.count(Filter(kinds = listOf(ContactCardEvent.KIND), authors = listOf(rec.providerPubKey))), - "retractions" to ctx.store.count(Filter(kinds = listOf(DeletionEvent.KIND), authors = listOf(rec.providerPubKey))), + "cards" to ctx.store.count(Filter(kinds = listOf(UserAssertionEvent.KIND), authors = listOf(rec.providerPubKey))), + "retractions" to ctx.store.count(Filter(kinds = listOf(DeletionRequestEvent.KIND), authors = listOf(rec.providerPubKey))), ) } diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/graperank/GrapeRankPublish.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/graperank/GrapeRankPublish.kt index f57b4eb3ce..dff06fcc6f 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/graperank/GrapeRankPublish.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/graperank/GrapeRankPublish.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEve import com.vitorpamplona.quartz.nip85TrustedAssertions.list.serviceProviders import com.vitorpamplona.quartz.nip85TrustedAssertions.list.tags.ProviderTypes import com.vitorpamplona.quartz.nip85TrustedAssertions.list.tags.ServiceProviderTag -import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent /** * The transport + consumption sides of NIP-85: `graperank publish` pushes the @@ -169,12 +169,12 @@ object GrapeRankPublish { val provider = providerArg?.let { ctx.requireUserHex(it) } val cardFilter = Filter( - kinds = listOf(ContactCardEvent.KIND), + kinds = listOf(UserAssertionEvent.KIND), tags = mapOf("d" to listOf(user)), authors = provider?.let { listOf(it) }, ) - suspend fun localCards(): List<ContactCardEvent> = ctx.store.query<Event>(cardFilter).filterIsInstance<ContactCardEvent>() + suspend fun localCards(): List<UserAssertionEvent> = ctx.store.query<Event>(cardFilter).filterIsInstance<UserAssertionEvent>() var cards = localCards() if (refresh || cards.isEmpty()) { @@ -190,7 +190,7 @@ object GrapeRankPublish { cards .groupBy { it.pubKey } .mapNotNull { (_, list) -> list.maxByOrNull { it.createdAt } } - .sortedWith(compareByDescending<ContactCardEvent> { it.rank() ?: -1 }.thenByDescending { it.createdAt }) + .sortedWith(compareByDescending<UserAssertionEvent> { it.rank() ?: -1 }.thenByDescending { it.createdAt }) // A provider key this machine's operator master derived maps back to // the observer whose subjective view the rank expresses. @@ -236,7 +236,7 @@ object GrapeRankPublish { if (!ctx.anonymous) { providerListOf(ctx, ctx.identity.pubKeyHex) ?.serviceProviders() - ?.filter { it.service.kind == ContactCardEvent.KIND && (provider == null || it.pubkey == provider) } + ?.filter { it.service.kind == UserAssertionEvent.KIND && (provider == null || it.pubkey == provider) } ?.map { it.relayUrl } .orEmpty() } else { diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/graperank/GrapeRankScore.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/graperank/GrapeRankScore.kt index 34efcf4f86..e23b4a3b06 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/graperank/GrapeRankScore.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/graperank/GrapeRankScore.kt @@ -34,8 +34,8 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent import com.vitorpamplona.quartz.nip09Deletions.DeletionIndex +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent import kotlin.math.roundToInt @@ -293,8 +293,8 @@ object GrapeRankScore { // Everything in the store already passed verifyAndStore, so mark the // deletions as verified and skip the redundant signature check. val deletions = DeletionIndex() - for (ev in ctx.store.query<Event>(Filter(kinds = listOf(DeletionEvent.KIND)))) { - if (ev is DeletionEvent) deletions.add(ev, wasVerified = true) + for (ev in ctx.store.query<Event>(Filter(kinds = listOf(DeletionRequestEvent.KIND)))) { + if (ev is DeletionRequestEvent) deletions.add(ev, wasVerified = true) } var dropped = 0 diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/account/AccountBootstrapEvents.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/account/AccountBootstrapEvents.kt index dbfb17605c..f5e80b7685 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/account/AccountBootstrapEvents.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/account/AccountBootstrapEvents.kt @@ -33,11 +33,11 @@ import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent -import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent +import com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent /** @@ -58,12 +58,12 @@ data class AccountBootstrapEvents( val userMetadata: MetadataEvent, val contactList: ContactListEvent, val nip65RelayList: AdvertisedRelayListEvent, - val dmRelayList: ChatMessageRelayListEvent, + val dmRelayList: DmRelayListEvent, val keyPackageRelayList: KeyPackageRelayListEvent, val searchRelayList: SearchRelayListEvent, val indexerRelayList: IndexerRelayListEvent, - val channelList: ChannelListEvent, - val relayFeedsList: RelayFeedsListEvent, + val channelList: PublicChatListEvent, + val relayFeedsList: FavoriteRelayListEvent, ) { /** All nine signed events in publication order. */ fun all(): List<com.vitorpamplona.quartz.nip01Core.core.Event> = @@ -102,13 +102,13 @@ fun bootstrapAccountEvents( signer = signer, ), nip65RelayList = AdvertisedRelayListEvent.create(DefaultNIP65List, signer), - dmRelayList = ChatMessageRelayListEvent.create(DefaultDMRelayList, signer), + dmRelayList = DmRelayListEvent.create(DefaultDMRelayList, signer), // MIP-00: advertise the default outbox relays as KeyPackage hosts // so other users can discover and fetch this account's KeyPackage // events without having to guess where they were published. keyPackageRelayList = KeyPackageRelayListEvent.create(DefaultNIP65RelaySet.toList(), signer), searchRelayList = SearchRelayListEvent.create(DefaultSearchRelayList.toList(), signer), indexerRelayList = IndexerRelayListEvent.create(DefaultIndexerRelayList.toList(), signer), - channelList = ChannelListEvent.create(emptyList(), DefaultChannels, signer), - relayFeedsList = RelayFeedsListEvent.create(DefaultGlobalRelays, signer), + channelList = PublicChatListEvent.create(emptyList(), DefaultChannels, signer), + relayFeedsList = FavoriteRelayListEvent.create(DefaultGlobalRelays, signer), ) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/ZapActions.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/ZapActions.kt index 5c6bd6ecc9..d9a75e71d8 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/ZapActions.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/ZapActions.kt @@ -25,13 +25,13 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent /** * NIP-57 zap-request building + LN address extraction. * - * Returns a signed [LnZapRequestEvent] (kind:9734) — the artifact a caller + * Returns a signed [ZapRequestEvent] (kind:9734) — the artifact a caller * hands to a LNURL-pay callback to receive a BOLT11 invoice. * * **Caller responsibilities** that the Amethyst Android flow handles but @@ -86,10 +86,10 @@ object ZapActions { amountMillisats: Long, inboxRelays: Set<NormalizedRelayUrl>, comment: String = "", - zapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PUBLIC, + zapType: ZapReceiptEvent.ZapType = ZapReceiptEvent.ZapType.PUBLIC, lnurl: String? = null, - ): LnZapRequestEvent = - LnZapRequestEvent.create( + ): ZapRequestEvent = + ZapRequestEvent.create( userHex = recipientPubkey, relays = inboxRelays, signer = signer, @@ -118,12 +118,12 @@ object ZapActions { amountMillisats: Long, inboxRelays: Set<NormalizedRelayUrl>, comment: String = "", - zapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PUBLIC, + zapType: ZapReceiptEvent.ZapType = ZapReceiptEvent.ZapType.PUBLIC, toUserPubkey: HexKey? = null, pollOption: Int? = null, lnurl: String? = null, - ): LnZapRequestEvent = - LnZapRequestEvent.create( + ): ZapRequestEvent = + ZapRequestEvent.create( zappedEvent = zappedEvent, relays = inboxRelays, signer = signer, @@ -142,7 +142,7 @@ object ZapActions { data class ZapRequestForSplit( val recipient: ZapSplitResolver.Recipient, val amountMillisats: Long, - val request: LnZapRequestEvent, + val request: ZapRequestEvent, ) /** @@ -172,7 +172,7 @@ object ZapActions { lookupLnAddress: suspend (HexKey) -> String?, lookupInboxRelays: suspend (HexKey) -> Set<NormalizedRelayUrl> = { emptySet() }, comment: String = "", - zapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PUBLIC, + zapType: ZapReceiptEvent.ZapType = ZapReceiptEvent.ZapType.PUBLIC, pollOption: Int? = null, ): List<ZapRequestForSplit> { val recipients = ZapSplitResolver.resolve(zappedEvent, lookupLnAddress) @@ -189,7 +189,7 @@ object ZapActions { val recipientInbox = recipient.pubkey?.let { lookupInboxRelays(it) }.orEmpty() val allRelays = senderInboxRelays + recipientInbox + authorInbox val request = - LnZapRequestEvent.create( + ZapRequestEvent.create( zappedEvent = zappedEvent, relays = allRelays, signer = signer, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/connectedApps/signers/NostrSignerPermissionLedger.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/connectedApps/signers/NostrSignerPermissionLedger.kt index 0991204ea0..1cfcd2f99e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/connectedApps/signers/NostrSignerPermissionLedger.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/connectedApps/signers/NostrSignerPermissionLedger.kt @@ -28,27 +28,27 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip25Reactions.ExternalReactionEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent -import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent +import com.vitorpamplona.quartz.nip38UserStatus.UserStatusEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent import com.vitorpamplona.quartz.nipA4PublicMessages.PublicMessageEvent @@ -232,7 +232,7 @@ class NostrSignerPermissionLedger( PublicMessageEvent.KIND, // 24 — NIP-A4 public messages (plaintext, public) ChannelMessageEvent.KIND, // 42 — public chat messages PollResponseEvent.KIND, // 1018 — voting in a poll (additive, like a reaction) - FileHeaderEvent.KIND, // 1063 — NIP-94 file metadata (shares a file reference) + FileMetadataEvent.KIND, // 1063 — NIP-94 file metadata (shares a file reference) PollEvent.KIND, // 1068 — creating a poll (additive public content) CommentEvent.KIND, // 1111 — NIP-22 threaded comments (same risk as kind 1) VoiceEvent.KIND, // 1222 — voice messages (audio post, like a picture/video) @@ -243,12 +243,12 @@ class NostrSignerPermissionLedger( TorrentEvent.KIND, // 2003 — NIP-35 torrent announcements (additive public content) TorrentCommentEvent.KIND, // 2004 — NIP-35 torrent comments HighlightEvent.KIND, // 9802 — highlighted snippets shared publicly - LnZapRequestEvent.KIND, // 9734 — Lightning zap request; the payment itself still prompts - LongTextNoteEvent.KIND, // 30023 — NIP-23 long-form articles (addressable content) - StatusEvent.KIND, // 30315 — ephemeral user status / presence - WikiNoteEvent.KIND, // 30818 — NIP-54 wiki articles (addressable content) - VideoHorizontalEvent.KIND, // 34235 — addressable horizontal video (NIP-71) - VideoVerticalEvent.KIND, // 34236 — addressable vertical video (NIP-71) + ZapRequestEvent.KIND, // 9734 — Lightning zap request; the payment itself still prompts + LongFormContentEvent.KIND, // 30023 — NIP-23 long-form articles (addressable content) + UserStatusEvent.KIND, // 30315 — ephemeral user status / presence + WikiArticleEvent.KIND, // 30818 — NIP-54 wiki articles (addressable content) + AddressableNormalVideoEvent.KIND, // 34235 — addressable horizontal video (NIP-71) + AddressableShortVideoEvent.KIND, // 34236 — addressable vertical video (NIP-71) PublicationIndexEvent.KIND, // 30040 — NKBIP-01 publication index (addressable content) LearningResourceEvent.KIND, // 30142 — learning resources (addressable content) BlossomPieceIndexEvent.KIND, // 32176 — Blossom piece indexes (addressable content) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/FeedContentState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/FeedContentState.kt index 02767f31d0..550621707b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/FeedContentState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/FeedContentState.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.amethyst.commons.service.BasicBundledInsert import com.vitorpamplona.amethyst.commons.service.BasicBundledUpdate import com.vitorpamplona.amethyst.commons.util.equalImmutableLists -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.utils.flattenToSet import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -149,7 +149,7 @@ class FeedContentState( val oldNotesState = _feedContent.value if (localFilter is AdditiveFeedFilter && lastFeedKey == localFilter.feedKey()) { if (oldNotesState is FeedState.Loaded) { - val deletionEvents: List<DeletionEvent> = newItems.mapNotNull { it.event as? DeletionEvent } + val deletionEvents: List<DeletionRequestEvent> = newItems.mapNotNull { it.event as? DeletionRequestEvent } val oldList = if (deletionEvents.isEmpty()) { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotManager.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotManager.kt index 29a82221cb..b100491b67 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotManager.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotManager.kt @@ -84,7 +84,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTags -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip18Reposts.quotes.QEventTag import com.vitorpamplona.quartz.nip18Reposts.quotes.quote import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler @@ -740,8 +740,8 @@ class MarmotManager( persistOwn: Boolean = true, ): TextMessageBundle { require(targetEvents.isNotEmpty()) { "buildDeletionMessage: targetEvents must not be empty" } - val template = DeletionEvent.build(targetEvents) - val innerEvent = RumorAssembler.assembleRumor<DeletionEvent>(signer.pubKey, template) + val template = DeletionRequestEvent.build(targetEvents) + val innerEvent = RumorAssembler.assembleRumor<DeletionRequestEvent>(signer.pubKey, template) val outbound = buildGroupMessage(nostrGroupId, innerEvent) if (persistOwn) persistDecryptedMessage(nostrGroupId, innerEvent.toJson()) return TextMessageBundle(outbound = outbound, innerEvent = innerEvent) @@ -1502,7 +1502,7 @@ class MarmotManager( val authorOf = HashMap<HexKey, HexKey>(messages.size) val claims = ArrayList<Pair<HexKey, HexKey>>() for (event in messages) { - if (event.kind == DeletionEvent.KIND) { + if (event.kind == DeletionRequestEvent.KIND) { for (tag in event.tags) { if (tag.size >= 2 && tag[0] == "e") claims.add(tag[1] to event.pubKey) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/DvmHeartbeat.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/DvmHeartbeat.kt index 449decb3cd..dcbcefa483 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/DvmHeartbeat.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/DvmHeartbeat.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.nip90DVMs.DvmHeartbeatRegistry import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.DvmContentDiscoveryRequestEvent import com.vitorpamplona.quartz.nip90Dvms.dvmHeartbeat.DvmHeartbeatEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -56,7 +56,7 @@ fun LocalCache.cachedDvmAnnouncements(limit: Int = 100): List<AppDefinitionEvent addressables .filterIntoSet(AppDefinitionEvent.KIND) { _, note -> (note.event as? AppDefinitionEvent)?.let { - it.appMetaData()?.subscription != true && it.includeKind(NIP90ContentDiscoveryRequestEvent.KIND) + it.appMetaData()?.subscription != true && it.includeKind(DvmContentDiscoveryRequestEvent.KIND) } == true }.mapNotNull { it.event as? AppDefinitionEvent } .sortedByDescending { it.createdAt } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/HomeFeedType.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/HomeFeedType.kt index eb2ebb6c06..204864b038 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/HomeFeedType.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/HomeFeedType.kt @@ -40,19 +40,19 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip64Chess.challenge.offer.LiveChessGameChallengeEvent import com.vitorpamplona.quartz.nip64Chess.end.LiveChessGameEndEvent import com.vitorpamplona.quartz.nip64Chess.game.ChessGameEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent @@ -80,10 +80,10 @@ enum class HomeFeedType( REPOSTS("reposts", listOf(RepostEvent.KIND, GenericRepostEvent.KIND)), COMMENTS("comments", listOf(CommentEvent.KIND)), PICTURES("pictures", listOf(PictureEvent.KIND)), - VIDEOS("videos", listOf(VideoNormalEvent.KIND, VideoHorizontalEvent.KIND)), - SHORTS("shorts", listOf(VideoShortEvent.KIND, VideoVerticalEvent.KIND)), - ARTICLES("articles", listOf(LongTextNoteEvent.KIND)), - WIKI("wiki", listOf(WikiNoteEvent.KIND)), + VIDEOS("videos", listOf(VideoNormalEvent.KIND, AddressableNormalVideoEvent.KIND)), + SHORTS("shorts", listOf(VideoShortEvent.KIND, AddressableShortVideoEvent.KIND)), + ARTICLES("articles", listOf(LongFormContentEvent.KIND)), + WIKI("wiki", listOf(WikiArticleEvent.KIND)), HIGHLIGHTS("highlights", listOf(HighlightEvent.KIND)), POLLS("polls", listOf(PollEvent.KIND, ZapPollEvent.KIND, PollResponseEvent.KIND)), CLASSIFIEDS("classifieds", listOf(ClassifiedsEvent.KIND)), diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt index 6b1e4d1640..54d776c06e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt @@ -24,11 +24,11 @@ import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupList import com.vitorpamplona.amethyst.commons.model.privateChats.ChatroomList import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response import com.vitorpamplona.quartz.nip57Zaps.IPrivateZapsDecryptionCache @@ -40,9 +40,9 @@ import com.vitorpamplona.quartz.utils.DualCase * Used by Note.kt for checking NWC payment status. */ interface INwcSignerState { - suspend fun decryptResponse(event: LnZapPaymentResponseEvent): Response? + suspend fun decryptResponse(event: NwcResponseEvent): Response? - suspend fun decryptRequest(event: LnZapPaymentRequestEvent): Request? + suspend fun decryptRequest(event: NwcRequestEvent): Request? fun isNIP47Author(pubKey: String?): Boolean } @@ -127,7 +127,7 @@ interface IAccount { fun isAcceptable(note: Note): Boolean /** Send a NIP-04 encrypted direct message */ - suspend fun sendNip04PrivateMessage(eventTemplate: EventTemplate<PrivateDmEvent>) + suspend fun sendNip04PrivateMessage(eventTemplate: EventTemplate<EncryptedDmEvent>) /** Send a NIP-17 gift-wrapped direct message */ suspend fun sendNip17PrivateMessage(template: EventTemplate<ChatMessageEvent>) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt index 3dde3a6d1e..342e88fda1 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt @@ -51,16 +51,16 @@ import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportType -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip59Giftwrap.HostStub import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent @@ -1038,7 +1038,7 @@ open class Note( } return anyAsync(zapPayments) { next -> - val zapResponseEvent = next.second?.event as? LnZapPaymentResponseEvent + val zapResponseEvent = next.second?.event as? NwcResponseEvent if (zapResponseEvent != null) { val response = account.nip47SignerState.decryptResponse(zapResponseEvent) @@ -1066,11 +1066,11 @@ open class Note( return false } - val parallelDecrypt = mutableListOf<Pair<LnZapRequestEvent, LnZapEvent>>() + val parallelDecrypt = mutableListOf<Pair<ZapRequestEvent, ZapReceiptEvent>>() zapEvents.forEach { next -> - val zapRequest = next.key.event as LnZapRequestEvent - val zapEvent = next.value?.event as? LnZapEvent + val zapRequest = next.key.event as ZapRequestEvent + val zapEvent = next.value?.event as? ZapReceiptEvent if (zapEvent != null) { if (!zapRequest.isPrivateZap()) { @@ -1227,7 +1227,7 @@ open class Note( // Regular Zap Receipts zaps.forEach { val noteEvent = it.value?.event - if (noteEvent is LnZapEvent) { + if (noteEvent is ZapReceiptEvent) { sumOfAmounts += noteEvent.amount ?: BigDecimal(0) } } @@ -1299,8 +1299,8 @@ open class Note( paymentResponse: Note?, signerState: INwcSignerState, ): InvoiceAmount? { - val nwcRequest = paymentRequest.event as? LnZapPaymentRequestEvent - val nwcResponse = paymentResponse?.event as? LnZapPaymentResponseEvent + val nwcRequest = paymentRequest.event as? NwcRequestEvent + val nwcResponse = paymentResponse?.event as? NwcResponseEvent return if (nwcRequest != null && nwcResponse != null) { processZapAmountFromResponse( @@ -1319,8 +1319,8 @@ open class Note( ) private suspend fun processZapAmountFromResponse( - nwcRequest: LnZapPaymentRequestEvent, - nwcResponse: LnZapPaymentResponseEvent, + nwcRequest: NwcRequestEvent, + nwcResponse: NwcResponseEvent, signerState: INwcSignerState, ): InvoiceAmount? { // if we can decrypt the reply @@ -1361,7 +1361,7 @@ open class Note( } val invoiceSet = LinkedHashSet<String>(zaps.size + zapPayments.size) - zaps.forEach { (it.value?.event as? LnZapEvent)?.lnInvoice()?.let { invoiceSet.add(it) } } + zaps.forEach { (it.value?.event as? ZapReceiptEvent)?.lnInvoice()?.let { invoiceSet.add(it) } } return zappedAmountCalculation( zapsAmount, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadAssembler.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadAssembler.kt index e07c1c98f9..d75e4ab742 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadAssembler.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadAssembler.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent @@ -183,7 +183,7 @@ class ThreadAssembler( */ fun Event?.anchorsItsOwnThread(): Boolean = when (this) { - is ReactionEvent, is LnZapEvent, is NutzapEvent, is OnchainZapEvent, is Bolt12ZapEvent -> true + is ReactionEvent, is ZapReceiptEvent, is NutzapEvent, is OnchainZapEvent, is Bolt12ZapEvent -> true else -> false } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt index ac221fef95..aaa614f854 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt @@ -37,7 +37,7 @@ import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent @@ -76,7 +76,7 @@ class User( val nip65RelayListNote: Note = context.addressableNote(AdvertisedRelayListEvent.createAddress(pubkeyHex)) - val dmRelayListNote: Note = context.addressableNote(ChatMessageRelayListEvent.createAddress(pubkeyHex)) + val dmRelayListNote: Note = context.addressableNote(DmRelayListEvent.createAddress(pubkeyHex)) val nutzapInfoNote: Note = context.addressableNote(NutzapInfoEvent.createAddress(pubkeyHex)) @@ -114,7 +114,7 @@ class User( fun pubkeyDisplayHex() = pubkeyNpub().toShortDisplay(5) - fun dmInboxRelayList() = dmRelayListNote.event as? ChatMessageRelayListEvent + fun dmInboxRelayList() = dmRelayListNote.event as? DmRelayListEvent fun authorRelayList() = nip65RelayListNote.event as? AdvertisedRelayListEvent diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/backups/ReplaceableBackupConflict.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/backups/ReplaceableBackupConflict.kt index dd348ce0da..f32456f1c1 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/backups/ReplaceableBackupConflict.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/backups/ReplaceableBackupConflict.kt @@ -32,18 +32,18 @@ import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip13Pow.tags.PoWTag -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent -import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent +import com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListEvent import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.nip40Expiration.ExpirationTag import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent import com.vitorpamplona.quartz.nip60Cashu.wallet.CashuWalletEvent @@ -93,18 +93,18 @@ enum class BackupEventType { ContactListEvent.KIND -> FOLLOW_LIST MuteListEvent.KIND -> MUTE_LIST AdvertisedRelayListEvent.KIND -> OUTBOX_INBOX_RELAYS - ChatMessageRelayListEvent.KIND -> DM_RELAYS + DmRelayListEvent.KIND -> DM_RELAYS KeyPackageRelayListEvent.KIND -> KEY_PACKAGE_RELAYS SearchRelayListEvent.KIND -> SEARCH_RELAYS IndexerRelayListEvent.KIND -> INDEXER_RELAYS - RelayFeedsListEvent.KIND -> RELAY_FEEDS + FavoriteRelayListEvent.KIND -> RELAY_FEEDS BlockedRelayListEvent.KIND -> BLOCKED_RELAYS TrustedRelayListEvent.KIND -> TRUSTED_RELAYS PrivateOutboxRelayListEvent.KIND -> PRIVATE_OUTBOX_RELAYS AppSpecificDataEvent.KIND -> APP_SETTINGS - ChannelListEvent.KIND -> PUBLIC_CHATS + PublicChatListEvent.KIND -> PUBLIC_CHATS CommunityListEvent.KIND -> COMMUNITIES - HashtagListEvent.KIND -> HASHTAGS + InterestListEvent.KIND -> HASHTAGS GeohashListEvent.KIND -> GEOHASHES FavoriteAlgoFeedsListEvent.KIND -> FAVORITE_ALGO_FEEDS EphemeralChatListEvent.KIND -> EPHEMERAL_CHATS diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt index 8f26268712..119f2ff33e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/CachePruner.kt @@ -35,19 +35,19 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUsers import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.quotes.taggedQuoteIds import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent +import com.vitorpamplona.quartz.nip38UserStatus.UserStatusEvent import com.vitorpamplona.quartz.nip40Expiration.isExpirationBefore import com.vitorpamplona.quartz.nip56Reports.ReportEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent -import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils @@ -207,7 +207,7 @@ class CachePruner( val roomNip04Pruned = HashMap<NormalizedRelayUrl, Long>() // chatroom.nip04History is lazy — only touch (allocate) it when this room actually drops a // kind:4 message, so rooms that never paged conversation history pay nothing. - val roomNip04Floor = if (toBeRemoved.any { it.event is PrivateDmEvent }) chatroom.nip04History.floor else null + val roomNip04Floor = if (toBeRemoved.any { it.event is EncryptedDmEvent }) chatroom.nip04History.floor else null toBeRemoved.forEach { note -> when (val ev = note.event) { @@ -216,7 +216,7 @@ class CachePruner( val outerUntil = note.rumorHost?.createdAt ?: ev.createdAt if (outerUntil < giftWrapFloor) note.relays.forEach { giftWrapPruned.mergeMax(it, outerUntil) } } - is PrivateDmEvent -> { + is EncryptedDmEvent -> { val until = ev.createdAt if (accountNip04Floor != null && until < accountNip04Floor) note.relays.forEach { accountNip04Pruned.mergeMax(it, until) } if (roomNip04Floor != null && until < roomNip04Floor) note.relays.forEach { roomNip04Pruned.mergeMax(it, until) } @@ -317,8 +317,8 @@ class CachePruner( ( (note.event is TextNoteEvent && !note.isNewThread()) || note.event is ReactionEvent || - note.event is LnZapEvent || - note.event is LnZapRequestEvent || + note.event is ZapReceiptEvent || + note.event is ZapRequestEvent || note.event is ReportEvent || note.event is GenericRepostEvent ) && @@ -413,11 +413,11 @@ class CachePruner( } } - if (note is AddressableNote && noteEvent is ContactCardEvent) { + if (note is AddressableNote && noteEvent is UserAssertionEvent) { cache.getUserIfExists(noteEvent.aboutUser())?.cardsOrNull()?.removeCard(note) } - if (note is AddressableNote && noteEvent is StatusEvent) { + if (note is AddressableNote && noteEvent is UserStatusEvent) { note.author?.statusStateOrNull()?.removeStatus(note) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt index d4aaf2d354..1789346dcf 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt @@ -221,28 +221,28 @@ import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip09Deletions.DeletionIndex +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip10Notes.BaseNoteEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.BaseRepostEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip18Reposts.quotes.taggedQuoteIds import com.vitorpamplona.quartz.nip19Bech32.isATag import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip25Reactions.ExternalReactionEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelHideMessageEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMuteUserEvent -import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent +import com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip29RelayGroups.GroupId import com.vitorpamplona.quartz.nip29RelayGroups.groupId @@ -251,19 +251,19 @@ import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMembersEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupParticipantsEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupPinnedEvent -import com.vitorpamplona.quartz.nip29RelayGroups.metadata.SupportedRolesEvent +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupRolesEvent import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateGroupEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateInviteEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.DeleteEventEvent import com.vitorpamplona.quartz.nip29RelayGroups.moderation.DeleteGroupEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.EditMetadataEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.PutUserEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.RemoveUserEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.UpdatePinListEvent -import com.vitorpamplona.quartz.nip29RelayGroups.request.JoinRequestEvent -import com.vitorpamplona.quartz.nip29RelayGroups.request.LeaveRequestEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupCreateInviteEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupDeleteEventEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupEditMetadataEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupPutUserEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupRemoveUserEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupUpdatePinListEvent +import com.vitorpamplona.quartz.nip29RelayGroups.request.GroupJoinRequestEvent +import com.vitorpamplona.quartz.nip29RelayGroups.request.GroupLeaveRequestEvent import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiListEvent import com.vitorpamplona.quartz.nip32Labeling.LabelEvent import com.vitorpamplona.quartz.nip34Git.grasp.UserGraspListEvent import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent @@ -278,31 +278,31 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent -import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent +import com.vitorpamplona.quartz.nip38UserStatus.UserStatusEvent import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent import com.vitorpamplona.quartz.nip40Expiration.isExpired import com.vitorpamplona.quartz.nip43RelayMembers.addMember.RelayAddMemberEvent import com.vitorpamplona.quartz.nip43RelayMembers.list.RelayMembershipListEvent import com.vitorpamplona.quartz.nip43RelayMembers.removeMember.RelayRemoveMemberEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent @@ -310,7 +310,7 @@ import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent import com.vitorpamplona.quartz.nip51Lists.videoCurationSet.VideoCurationSetEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import com.vitorpamplona.quartz.nip52Calendar.rsvp.CalendarRSVPEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent @@ -320,13 +320,13 @@ import com.vitorpamplona.quartz.nip53LiveActivities.nestsServers.NestsServersEve import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent import com.vitorpamplona.quartz.nip53LiveActivities.raid.LiveActivitiesRaidEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip54Wiki.WikiMergeAcceptanceEvent import com.vitorpamplona.quartz.nip54Wiki.WikiMergeRequestEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip54Wiki.WikiRedirectEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.validate.LnZapReceiptValidator import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointResolver import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlForm @@ -335,7 +335,7 @@ import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent import com.vitorpamplona.quartz.nip59Giftwrap.HasInnerEvent -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent @@ -360,20 +360,20 @@ import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.RelayDiscoveryEvent import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.RelayMonitorEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip71Video.textTrack.TextTrackEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent -import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent +import com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent -import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent import com.vitorpamplona.quartz.nip87Ecash.fedimint.FedimintEvent import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent @@ -381,13 +381,13 @@ import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.DvmContentDiscoveryRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip90Dvms.dvmHeartbeat.DvmHeartbeatEvent -import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent -import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryRequest.NIP90UserDiscoveryRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryResponse.NIP90UserDiscoveryResponseEvent -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import com.vitorpamplona.quartz.nip90Dvms.status.DvmStatusEvent +import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryRequest.DvmUserDiscoveryRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryResponse.DvmUserDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent @@ -502,7 +502,7 @@ open class EventCache : val bolt12ZapValidator = Bolt12ZapValidator() /** - * Resolver for LNURL provider metadata used by [consume]`(LnZapEvent)` to + * Resolver for LNURL provider metadata used by [consume]`(ZapReceiptEvent)` to * validate NIP-57 Appendix F. `null` skips the receipt-signer check (the * receipt is still accepted on signature verification alone — matches legacy * behavior); set this in app init so receipts can be verified against the @@ -1210,7 +1210,7 @@ open class EventCache : } fun consume( - event: LongTextNoteEvent, + event: LongFormContentEvent, relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { @@ -1298,7 +1298,7 @@ open class EventCache : } fun consume( - event: WikiNoteEvent, + event: WikiArticleEvent, relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { @@ -1347,7 +1347,7 @@ open class EventCache : event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } } - is LongTextNoteEvent -> { + is LongFormContentEvent -> { event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } } @@ -1405,7 +1405,7 @@ open class EventCache : event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } } - is LnZapEvent -> { + is ZapReceiptEvent -> { event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().map { getOrCreateAddressableNote(it) } + (event.zapRequest?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptyList()) @@ -1438,11 +1438,11 @@ open class EventCache : // The zapped event is carried in the kind:9321's `e` tags // (and optionally an `a` tag for addressables). Whichever // notes those resolve to receive the nutzap entry — - // analogous to how LnZapEvent flows into `addZap`. + // analogous to how ZapReceiptEvent flows into `addZap`. event.linkedEventIds().mapNotNull { checkGetOrCreateNote(it) } } - is LnZapRequestEvent -> { + is ZapRequestEvent -> { event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().map { getOrCreateAddressableNote(it) } } @@ -1456,7 +1456,7 @@ open class EventCache : event.awardDefinition().map { getOrCreateAddressableNote(it) } } - is PrivateDmEvent -> { + is EncryptedDmEvent -> { event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } } @@ -1557,7 +1557,7 @@ open class EventCache : } fun consume( - event: StatusEvent, + event: UserStatusEvent, relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { @@ -1577,7 +1577,7 @@ open class EventCache : fun AddressableEvent.toAddressableNote() = getOrCreateAddressableNote(address()) fun consume( - event: ContactCardEvent, + event: UserAssertionEvent, relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { @@ -1667,7 +1667,7 @@ open class EventCache : } fun consume( - event: DeletionEvent, + event: DeletionRequestEvent, relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { @@ -2245,13 +2245,13 @@ open class EventCache : /** NIP-29 relay-declared supported roles (kind 39003) → the group's role set. */ fun consume( - event: SupportedRolesEvent, + event: GroupRolesEvent, relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val new = consumeBaseReplaceable(event, relay, wasVerified) if (relay != null && isRelaySignedGroupEvent(event, relay)) { - val latest = getOrCreateAddressableNote(event.address()).event as? SupportedRolesEvent + val latest = getOrCreateAddressableNote(event.address()).event as? GroupRolesEvent latest?.let { getOrCreateRelayGroupChannel(GroupId(it.groupId(), relay)).updateSupportedRoles(it) } } return new @@ -2661,7 +2661,7 @@ open class EventCache : ): Boolean = false fun consume( - event: LnZapEvent, + event: ZapReceiptEvent, relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { @@ -2687,7 +2687,7 @@ open class EventCache : val zapRequest = event.zapRequest?.id?.let { getNoteIfExists(it) } - if (zapRequest == null || zapRequest.event !is LnZapRequestEvent) { + if (zapRequest == null || zapRequest.event !is ZapRequestEvent) { Log.d("ZP") { "Zap Request not found. Unable to process Zap {${event.toJson()}}" } return false } @@ -2788,7 +2788,7 @@ open class EventCache : * zap target). Returns null if we have no metadata, or the user has neither * lud16 nor lud06. */ - private fun recipientLnurl(event: LnZapEvent): String? { + private fun recipientLnurl(event: ZapReceiptEvent): String? { val recipientPubkey = event.zappedAuthor().firstOrNull() ?: return null val user = getUserIfExists(recipientPubkey) ?: return null return user.lnAddress()?.takeIf { it.isNotBlank() } @@ -2925,7 +2925,7 @@ open class EventCache : } private fun attachZapToLiveActivityChannel( - event: LnZapEvent, + event: ZapReceiptEvent, note: Note, relay: NormalizedRelayUrl?, ) { @@ -2963,7 +2963,7 @@ open class EventCache : } fun consume( - event: LnZapRequestEvent, + event: ZapRequestEvent, relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { @@ -3166,7 +3166,7 @@ open class EventCache : } fun consume( - event: LnZapPaymentRequestEvent, + event: NwcRequestEvent, relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { @@ -3175,11 +3175,11 @@ open class EventCache : } fun consume( - event: LnZapPaymentRequestEvent, + event: NwcRequestEvent, zappedNote: Note?, wasVerified: Boolean, relay: NormalizedRelayUrl?, - onResponse: suspend (LnZapPaymentResponseEvent) -> Unit, + onResponse: suspend (NwcResponseEvent) -> Unit, ): Boolean { val note = getOrCreateNote(event.id) val author = getOrCreateUser(event.pubKey) @@ -3213,7 +3213,7 @@ open class EventCache : } fun consume( - event: LnZapPaymentResponseEvent, + event: NwcResponseEvent, relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { @@ -3285,7 +3285,7 @@ open class EventCache : return false } - fun getPeopleListNotesFor(user: User): List<AddressableNote> = addressables.filter(PeopleListEvent.KIND, user.pubkeyHex) + fun getPeopleListNotesFor(user: User): List<AddressableNote> = addressables.filter(FollowSetEvent.KIND, user.pubkeyHex) override fun markAsSeen( eventId: String, @@ -3584,7 +3584,7 @@ open class EventCache : is ChannelMetadataEvent -> consume(event, relay, wasVerified) is ChannelMuteUserEvent -> consume(event, relay, wasVerified) is CommunityPostApprovalEvent -> consume(event, relay, wasVerified) - is DeletionEvent -> consume(event, relay, wasVerified) + is DeletionRequestEvent -> consume(event, relay, wasVerified) is DraftWrapEvent -> consume(event, relay, wasVerified) is EphemeralChatEvent -> consume(event, relay, wasVerified) is GeohashChatEvent -> consume(event, relay, wasVerified) @@ -3595,7 +3595,7 @@ open class EventCache : // 39003 (relay-declared roles) is durable group state like 39000/39001/39002: // route it onto the channel so a moderation UI can offer the relay's role set. - is SupportedRolesEvent -> consume(event, relay, wasVerified) + is GroupRolesEvent -> consume(event, relay, wasVerified) is GenericRepostEvent -> consume(event, relay, wasVerified) is FileStorageEvent -> consume(event, relay, wasVerified) @@ -3627,26 +3627,26 @@ open class EventCache : } } - is LnZapEvent -> consume(event, relay, wasVerified) - is LnZapRequestEvent -> consume(event, relay, wasVerified) + is ZapReceiptEvent -> consume(event, relay, wasVerified) + is ZapRequestEvent -> consume(event, relay, wasVerified) is OnchainZapEvent -> consume(event, relay, wasVerified) is Bolt12ZapEvent -> consume(event, relay, wasVerified) - is LnZapPaymentRequestEvent -> consume(event, relay, wasVerified) - is LnZapPaymentResponseEvent -> consume(event, relay, wasVerified) - is LongTextNoteEvent -> consume(event, relay, wasVerified) + is NwcRequestEvent -> consume(event, relay, wasVerified) + is NwcResponseEvent -> consume(event, relay, wasVerified) + is LongFormContentEvent -> consume(event, relay, wasVerified) is MetadataEvent -> consume(event, relay, wasVerified) is NipTextEvent -> consume(event, relay, wasVerified) is OtsEvent -> consume(event, relay, wasVerified) is PollResponseEvent -> consume(event, relay, wasVerified) is ReactionEvent -> consume(event, relay, wasVerified) is LabelEvent -> consume(event, relay, wasVerified) - is ContactCardEvent -> consume(event, relay, wasVerified) + is UserAssertionEvent -> consume(event, relay, wasVerified) is ReportEvent -> consume(event, relay, wasVerified) is RepostEvent -> consume(event, relay, wasVerified) - is StatusEvent -> consume(event, relay, wasVerified) + is UserStatusEvent -> consume(event, relay, wasVerified) is TextNoteModificationEvent -> consume(event, relay, wasVerified) is ConcordChatEditEvent -> consume(event, relay, wasVerified) - is WikiNoteEvent -> consume(event, relay, wasVerified) + is WikiArticleEvent -> consume(event, relay, wasVerified) is PaymentTargetsEvent -> consume(event, relay, wasVerified) is ChatEvent -> { @@ -3671,7 +3671,7 @@ open class EventCache : // Buzz workspace kinds (block/buzz — the Buzz dialect of NIP-29). // Timeline kinds attach into the group's BuzzWorkspaceChannel; the // rest are stored for query/state. Kinds 9041/39005/49001 are absent - // on purpose: their numbers belong to GoalEvent, GroupPinnedEvent and + // on purpose: their numbers belong to ZapGoalEvent, GroupPinnedEvent and // a non-wire audit kind. Kind 20001 is shared with GeohashPresenceEvent // (BitChat); EventFactory routes it to PresenceUpdateEvent only when the // BitChat `g` tag is absent, and it is handled below with the ephemerals. @@ -3799,14 +3799,14 @@ open class EventCache : is BroadcastRelayListEvent, is BookmarkListEvent, is OldBookmarkListEvent, - is CalendarEvent, + is CalendarCollectionEvent, is CalendarDateSlotEvent, is CalendarTimeSlotEvent, is CalendarRSVPEvent, is CashuWalletEvent, is NutzapInfoEvent, - is ChannelListEvent, - is ChatMessageRelayListEvent, + is PublicChatListEvent, + is DmRelayListEvent, is Bolt12OfferListEvent, is ClassifiedsEvent, is FundraiserEvent, @@ -3825,7 +3825,7 @@ open class EventCache : DvmHeartbeatRegistry.record(event.address(), event.createdAt) } is EmojiPackEvent, - is EmojiPackSelectionEvent, + is EmojiListEvent, is EphemeralChatListEvent, // NIP-51 "simple groups" list (kind 10009): the user's joined NIP-29 groups + // servers. Replaceable like its sibling lists; RelayGroupListState reads it from the @@ -3840,7 +3840,7 @@ open class EventCache : is GroupParticipantsEvent, is ExternalIdentitiesEvent, is FileServersEvent, - is FollowListEvent, + is StarterPackEvent, is GeocacheListingEvent, is GeocacheCurationListEvent, is GeohashListEvent, @@ -3854,7 +3854,7 @@ open class EventCache : is NamedSiteEvent, is RootNappletEvent, is NamedNappletEvent, - is RelayFeedsListEvent, + is FavoriteRelayListEvent, is KeyPackageEvent, is KeyPackageRelayListEvent, is LiveChessGameChallengeEvent, @@ -3862,14 +3862,14 @@ open class EventCache : is LiveChessMoveEvent, is LiveChessGameEndEvent, is LiveChessDrawOfferEvent, - is HashtagListEvent, + is InterestListEvent, is FavoriteAlgoFeedsListEvent, is IndexerRelayListEvent, is InteractiveStoryPrologueEvent, is InteractiveStorySceneEvent, is InteractiveStoryReadingStateEvent, is InterestSetEvent, - is LabeledBookmarkListEvent, + is BookmarkSetEvent, is MeetingSpaceEvent, is MeetingRoomEvent, is MusicTrackEvent, @@ -3884,7 +3884,7 @@ open class EventCache : is ProfileBadgesEvent, is ProxyRelayListEvent, is PinListEvent, - is PeopleListEvent, + is FollowSetEvent, // Buzz addressable/replaceable state. is PersonaEvent, is TeamEvent, @@ -3904,8 +3904,8 @@ open class EventCache : is SoftwareApplicationEvent, is TrustedRelayListEvent, is TrustProviderListEvent, - is VideoHorizontalEvent, - is VideoVerticalEvent, + is AddressableNormalVideoEvent, + is AddressableShortVideoEvent, is TextTrackEvent, is VideoCollaborationEvent, is VideoCurationSetEvent, @@ -3953,21 +3953,21 @@ open class EventCache : // one-shot events the relay is authoritative for (it applies them and // republishes the 39000/39001/39002); we store them so they're queryable, // but we don't act on them client-side. - is PutUserEvent, - is RemoveUserEvent, - is EditMetadataEvent, - is DeleteEventEvent, - is UpdatePinListEvent, + is GroupPutUserEvent, + is GroupRemoveUserEvent, + is GroupEditMetadataEvent, + is GroupDeleteEventEvent, + is GroupUpdatePinListEvent, is DeleteGroupEvent, is CreateGroupEvent, - is CreateInviteEvent, - is JoinRequestEvent, - is LeaveRequestEvent, + is GroupCreateInviteEvent, + is GroupJoinRequestEvent, + is GroupLeaveRequestEvent, is FhirResourceEvent, - is FileHeaderEvent, + is FileMetadataEvent, is ProfileGalleryEntryEvent, is FileStorageHeaderEvent, - is GoalEvent, + is ZapGoalEvent, is GroupEvent, is GitIssueEvent, is GitReplyEvent, @@ -3980,13 +3980,13 @@ open class EventCache : is JesterEvent, is HighlightEvent, is PodcastEpisodeEvent, - is NIP90StatusEvent, - is NIP90ContentDiscoveryResponseEvent, - is NIP90ContentDiscoveryRequestEvent, - is NIP90UserDiscoveryResponseEvent, - is NIP90UserDiscoveryRequestEvent, + is DvmStatusEvent, + is DvmContentDiscoveryResponseEvent, + is DvmContentDiscoveryRequestEvent, + is DvmUserDiscoveryResponseEvent, + is DvmUserDiscoveryRequestEvent, is PictureEvent, - is PrivateDmEvent, + is EncryptedDmEvent, is PublicMessageEvent, is RequestToVanishEvent, is CodeSnippetEvent, @@ -4000,7 +4000,7 @@ open class EventCache : is GeocacheVerificationEvent, is RoadEventReportEvent, is RoadEventConfirmationEvent, - is SealedRumorEvent, + is SealEvent, is SoftwareAssetEvent, is ExternalReactionEvent, is ExternalCitationEvent, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip23LongContent/LongFormPublishAction.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip23LongContent/LongFormPublishAction.kt index f34b9a0792..4526e93e9c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip23LongContent/LongFormPublishAction.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip23LongContent/LongFormPublishAction.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.commons.model.nip23LongContent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtag -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.utils.TimeUtils /** @@ -42,7 +42,7 @@ object LongFormPublishAction { * @param tags List of hashtag topics * @param dTag Unique identifier for this addressable event (slug) * @param signer The NostrSigner to sign the event - * @return Signed LongTextNoteEvent ready to broadcast + * @return Signed LongFormContentEvent ready to broadcast * @throws IllegalStateException if signer is not writeable */ suspend fun publish( @@ -53,7 +53,7 @@ object LongFormPublishAction { tags: List<String>, dTag: String, signer: NostrSigner, - ): LongTextNoteEvent { + ): LongFormContentEvent { if (!signer.isWriteable()) { throw IllegalStateException("Cannot publish: signer is not writeable") } @@ -63,7 +63,7 @@ object LongFormPublishAction { } val template = - LongTextNoteEvent.build( + LongFormContentEvent.build( description = content, title = title, summary = summary, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListDecryptionCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListDecryptionCache.kt index e8f16823c9..702240aee3 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListDecryptionCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListDecryptionCache.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.commons.model.nip28PublicChats import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent +import com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListEvent import com.vitorpamplona.quartz.nip28PublicChat.list.channelSet import com.vitorpamplona.quartz.nip28PublicChat.list.channels import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache @@ -29,13 +29,13 @@ import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache class PublicChatListDecryptionCache( val signer: NostrSigner, ) { - val cachedPrivateLists = PrivateTagArrayEventCache<ChannelListEvent>(signer) + val cachedPrivateLists = PrivateTagArrayEventCache<PublicChatListEvent>(signer) - fun cachedChannelSet(event: ChannelListEvent) = cachedPrivateLists.mergeTagListPrecached(event).channelSet() + fun cachedChannelSet(event: PublicChatListEvent) = cachedPrivateLists.mergeTagListPrecached(event).channelSet() - fun cachedChannels(event: ChannelListEvent) = cachedPrivateLists.mergeTagListPrecached(event).channels() + fun cachedChannels(event: PublicChatListEvent) = cachedPrivateLists.mergeTagListPrecached(event).channels() - suspend fun channelSet(event: ChannelListEvent) = cachedPrivateLists.mergeTagList(event).channelSet() + suspend fun channelSet(event: PublicChatListEvent) = cachedPrivateLists.mergeTagList(event).channelSet() - suspend fun channels(event: ChannelListEvent) = cachedPrivateLists.mergeTagList(event).channels() + suspend fun channels(event: PublicChatListEvent) = cachedPrivateLists.mergeTagList(event).channels() } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt index 2c59434441..1f70bcea4b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent +import com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListEvent import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope @@ -44,9 +44,9 @@ import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch interface PublicChatListRepository { - fun channelList(): ChannelListEvent? + fun channelList(): PublicChatListEvent? - fun updateChannelListTo(newChannelList: ChannelListEvent?) + fun updateChannelListTo(newChannelList: PublicChatListEvent?) } class PublicChatListState( @@ -59,14 +59,14 @@ class PublicChatListState( // Creates a long-term reference for this note so that the GC doesn't collect the note it self val publicChatListNote = cache.getOrCreateAddressableNote(getChannelListAddress()) - fun getChannelListAddress() = ChannelListEvent.createAddress(signer.pubKey) + fun getChannelListAddress() = PublicChatListEvent.createAddress(signer.pubKey) fun getChannelListFlow(): StateFlow<NoteState> = publicChatListNote.flow().metadata.stateFlow - fun getChannelList(): ChannelListEvent? = publicChatListNote.event as? ChannelListEvent + fun getChannelList(): PublicChatListEvent? = publicChatListNote.event as? PublicChatListEvent suspend fun publicChatListWithBackup(note: Note): Set<ChannelTag> { - val event = note.event as? ChannelListEvent ?: settings.channelList() + val event = note.event as? PublicChatListEvent ?: settings.channelList() return event?.let { decryptionCache.channelSet(it) } ?: emptySet() } @@ -111,32 +111,32 @@ class PublicChatListState( emptyList(), ) - suspend fun follow(channel: PublicChatChannel): ChannelListEvent { + suspend fun follow(channel: PublicChatChannel): PublicChatListEvent { val publicChatList = getChannelList() return if (publicChatList == null) { - ChannelListEvent.create(ChannelTag(channel.idHex, channel.relayHintUrl()), true, signer) + PublicChatListEvent.create(ChannelTag(channel.idHex, channel.relayHintUrl()), true, signer) } else { - ChannelListEvent.add(publicChatList, ChannelTag(channel.idHex, channel.relayHintUrl()), true, signer) + PublicChatListEvent.add(publicChatList, ChannelTag(channel.idHex, channel.relayHintUrl()), true, signer) } } - suspend fun follow(channels: List<PublicChatChannel>): ChannelListEvent { + suspend fun follow(channels: List<PublicChatChannel>): PublicChatListEvent { val publicChatList = getChannelList() val channelTags = channels.map { ChannelTag(it.idHex, it.relayHintUrl()) } return if (publicChatList == null) { - ChannelListEvent.create(channelTags, true, signer) + PublicChatListEvent.create(channelTags, true, signer) } else { - ChannelListEvent.add(publicChatList, channelTags, true, signer) + PublicChatListEvent.add(publicChatList, channelTags, true, signer) } } - suspend fun unfollow(channel: PublicChatChannel): ChannelListEvent? { + suspend fun unfollow(channel: PublicChatChannel): PublicChatListEvent? { val publicChatList = getChannelList() return if (publicChatList != null) { - ChannelListEvent.remove(publicChatList, ChannelTag(channel.idHex, channel.relayHintUrl()), signer) + PublicChatListEvent.remove(publicChatList, ChannelTag(channel.idHex, channel.relayHintUrl()), signer) } else { null } @@ -155,7 +155,7 @@ class PublicChatListState( Log.d("AccountRegisterObservers", "Channel List Collector Start") getChannelListFlow().collect { Log.d("AccountRegisterObservers") { "Channel List for ${signer.pubKey}" } - (it.note.event as? ChannelListEvent)?.let { + (it.note.event as? PublicChatListEvent)?.let { settings.updateChannelListTo(it) } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/RelayGroupChannel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/RelayGroupChannel.kt index 9c029296f8..d1009af4cc 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/RelayGroupChannel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/RelayGroupChannel.kt @@ -35,7 +35,7 @@ import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupAdminsEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMembersEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupPinnedEvent -import com.vitorpamplona.quartz.nip29RelayGroups.metadata.SupportedRolesEvent +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupRolesEvent import com.vitorpamplona.quartz.nip29RelayGroups.tags.GroupAdminTag import com.vitorpamplona.quartz.nip29RelayGroups.tags.RoleTag import com.vitorpamplona.quartz.utils.cache.LargeCache @@ -237,7 +237,7 @@ class RelayGroupChannel( updateChannelInfo() } - fun updateSupportedRoles(event: SupportedRolesEvent) { + fun updateSupportedRoles(event: GroupRolesEvent) { // Only newer definitions supersede; equal-or-older is dropped (no redundant emit). if (event.createdAt <= supportedRolesUpdatedAt) return supportedRoles = event.roles() diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiPackState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiPackState.kt index decf5f345e..612e2c5673 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiPackState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiPackState.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiListEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -55,13 +55,13 @@ class EmojiPackState( // Creates a long-term reference for this note so that the GC doesn't collect the note it self val emojiPackListNote = cache.getOrCreateAddressableNote(getEmojiPackSelectionAddress()) - fun getEmojiPackSelectionAddress() = EmojiPackSelectionEvent.createAddress(signer.pubKey) + fun getEmojiPackSelectionAddress() = EmojiListEvent.createAddress(signer.pubKey) - fun getEmojiPackSelection(): EmojiPackSelectionEvent? = emojiPackListNote.event as? EmojiPackSelectionEvent + fun getEmojiPackSelection(): EmojiListEvent? = emojiPackListNote.event as? EmojiListEvent fun getEmojiPackSelectionFlow(): StateFlow<NoteState> = emojiPackListNote.flow().metadata.stateFlow - fun convertEmojiSelectionPack(selection: EmojiPackSelectionEvent?): List<StateFlow<NoteState>>? = + fun convertEmojiSelectionPack(selection: EmojiListEvent?): List<StateFlow<NoteState>>? = selection?.taggedAddresses()?.map { cache .getOrCreateAddressableNote(it) @@ -73,7 +73,7 @@ class EmojiPackState( val flow: StateFlow<List<StateFlow<NoteState>>?> = getEmojiPackSelectionFlow() .transformLatest { - emit(convertEmojiSelectionPack(it.note.event as? EmojiPackSelectionEvent)) + emit(convertEmojiSelectionPack(it.note.event as? EmojiListEvent)) }.onStart { emit(convertEmojiSelectionPack(getEmojiPackSelection())) }.flowOn(Dispatchers.IO) @@ -143,7 +143,7 @@ class EmojiPackState( } } - suspend fun addEmojiPack(emojiPack: Note): EmojiPackSelectionEvent { + suspend fun addEmojiPack(emojiPack: Note): EmojiListEvent { val emojiPackEvent = emojiPack.event if (emojiPackEvent !is EmojiPackEvent) throw IllegalArgumentException("Note is not an EmojiPackEvent; cannot add to emoji list.") @@ -151,21 +151,21 @@ class EmojiPackState( val usersEmojiList = getEmojiPackSelection() return if (usersEmojiList == null) { - val template = EmojiPackSelectionEvent.build(listOf(eventHint)) + val template = EmojiListEvent.build(listOf(eventHint)) signer.sign(template) } else { - val template = EmojiPackSelectionEvent.add(usersEmojiList, eventHint) + val template = EmojiListEvent.add(usersEmojiList, eventHint) signer.sign(template) } } - suspend fun removeEmojiPack(emojiPack: Note): EmojiPackSelectionEvent? { + suspend fun removeEmojiPack(emojiPack: Note): EmojiListEvent? { val usersEmojiList = getEmojiPackSelection() ?: throw IllegalArgumentException("No emoji pack selection exists to remove from.") val emojiPackEvent = emojiPack.event if (emojiPackEvent !is EmojiPackEvent) return null - val template = EmojiPackSelectionEvent.remove(usersEmojiList, emojiPackEvent) + val template = EmojiListEvent.remove(usersEmojiList, emojiPackEvent) return signer.sign(template) } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt index 0040e9e0d8..4fac63e096 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt @@ -23,38 +23,38 @@ package com.vitorpamplona.amethyst.commons.model.nip38UserStatuses import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent -import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent +import com.vitorpamplona.quartz.nip38UserStatus.UserStatusEvent class UserStatusAction { companion object { suspend fun create( newStatus: String, signer: NostrSigner, - type: String = StatusEvent.GENERAL, - ): StatusEvent = StatusEvent.create(newStatus, type, signer = signer) + type: String = UserStatusEvent.GENERAL, + ): UserStatusEvent = UserStatusEvent.create(newStatus, type, signer = signer) suspend fun update( oldStatus: AddressableNote, newStatus: String, signer: NostrSigner, - ): StatusEvent { - val oldEvent = oldStatus.event as? StatusEvent ?: throw IllegalStateException("Tried to update a non-status event") + ): UserStatusEvent { + val oldEvent = oldStatus.event as? UserStatusEvent ?: throw IllegalStateException("Tried to update a non-status event") - return StatusEvent.update(oldEvent, newStatus, signer) + return UserStatusEvent.update(oldEvent, newStatus, signer) } suspend fun delete( oldStatus: AddressableNote, signer: NostrSigner, ): List<Event> { - val oldEvent = oldStatus.event as? StatusEvent ?: throw IllegalStateException("Tried to update a non-status event") + val oldEvent = oldStatus.event as? UserStatusEvent ?: throw IllegalStateException("Tried to update a non-status event") - val event = StatusEvent.clear(oldEvent, signer) + val event = UserStatusEvent.clear(oldEvent, signer) val deletion = signer.sign( - DeletionEvent.buildForVersionOnly(listOf(event)), + DeletionRequestEvent.buildForVersionOnly(listOf(event)), ) return listOf(event, deletion) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt index a1d0403978..0d36cb6391 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag import com.vitorpamplona.quartz.nip51Lists.muteList.tags.WordTag -import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO @@ -48,14 +48,14 @@ class BlockPeopleListState( // Creates a long-term reference for this note so that the GC doesn't collect the note it self val blockListNote = cache.getOrCreateAddressableNote(getBlockListAddress()) - fun getBlockListAddress() = PeopleListEvent.createBlockAddress(signer.pubKey) + fun getBlockListAddress() = FollowSetEvent.createBlockAddress(signer.pubKey) fun getBlockListFlow(): StateFlow<NoteState> = blockListNote.flow().metadata.stateFlow - fun getBlockList(): PeopleListEvent? = blockListNote.event as? PeopleListEvent + fun getBlockList(): FollowSetEvent? = blockListNote.event as? FollowSetEvent suspend fun blockListWithBackup(note: Note): List<MuteTag> { - val event = note.event as? PeopleListEvent + val event = note.event as? FollowSetEvent return event?.let { decryptionCache.usersAndWords(it) } ?: emptyList() } @@ -70,32 +70,32 @@ class BlockPeopleListState( emptyList(), ) - suspend fun hideUser(pubkeyHex: String): PeopleListEvent { + suspend fun hideUser(pubkeyHex: String): FollowSetEvent { val blockList = getBlockList() return if (blockList != null) { - PeopleListEvent.add( + FollowSetEvent.add( earlierVersion = blockList, person = UserTag(pubkeyHex), isPrivate = true, signer = signer, ) } else { - PeopleListEvent.create( - title = PeopleListEvent.BLOCK_LIST_D_TAG, + FollowSetEvent.create( + title = FollowSetEvent.BLOCK_LIST_D_TAG, person = UserTag(pubkeyHex), isPrivate = true, signer = signer, - dTag = PeopleListEvent.BLOCK_LIST_D_TAG, + dTag = FollowSetEvent.BLOCK_LIST_D_TAG, ) } } - suspend fun showUser(pubkeyHex: String): PeopleListEvent? { + suspend fun showUser(pubkeyHex: String): FollowSetEvent? { val blockList = getBlockList() return if (blockList != null) { - PeopleListEvent.remove( + FollowSetEvent.remove( earlierVersion = blockList, person = UserTag(pubkeyHex), signer = signer, @@ -105,11 +105,11 @@ class BlockPeopleListState( } } - suspend fun showWord(word: String): PeopleListEvent? { + suspend fun showWord(word: String): FollowSetEvent? { val blockList = getBlockList() return if (blockList != null) { - PeopleListEvent.remove( + FollowSetEvent.remove( earlierVersion = blockList, person = WordTag(word), signer = signer, @@ -119,20 +119,20 @@ class BlockPeopleListState( } } - suspend fun showUsers(pubkeys: List<String>): PeopleListEvent? { + suspend fun showUsers(pubkeys: List<String>): FollowSetEvent? { if (pubkeys.isEmpty()) return null val blockList = getBlockList() ?: return null - return PeopleListEvent.removeAll( + return FollowSetEvent.removeAll( earlierVersion = blockList, persons = pubkeys.map { UserTag(it) }, signer = signer, ) } - suspend fun showWords(words: List<String>): PeopleListEvent? { + suspend fun showWords(words: List<String>): FollowSetEvent? { if (words.isEmpty()) return null val blockList = getBlockList() ?: return null - return PeopleListEvent.removeAll( + return FollowSetEvent.removeAll( earlierVersion = blockList, persons = words.map { WordTag(it) }, signer = signer, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt index 6aa9da1171..db80d69a1b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt @@ -22,15 +22,15 @@ package com.vitorpamplona.amethyst.commons.model.nip51Lists.hashtagLists import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.hashtagList.hashtagSet class HashtagListDecryptionCache( val signer: NostrSigner, ) { - val cachedPrivateLists = PrivateTagArrayEventCache<HashtagListEvent>(signer) + val cachedPrivateLists = PrivateTagArrayEventCache<InterestListEvent>(signer) - fun cachedHashtags(event: HashtagListEvent) = cachedPrivateLists.mergeTagListPrecached(event).hashtagSet() + fun cachedHashtags(event: InterestListEvent) = cachedPrivateLists.mergeTagListPrecached(event).hashtagSet() - suspend fun hashtags(event: HashtagListEvent) = cachedPrivateLists.mergeTagList(event).hashtagSet() + suspend fun hashtags(event: InterestListEvent) = cachedPrivateLists.mergeTagList(event).hashtagSet() } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt index e8f1af7638..e31ca57983 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt @@ -27,34 +27,34 @@ import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUsers import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUsersAndWords import com.vitorpamplona.quartz.nip51Lists.muteList.mutedWordSet import com.vitorpamplona.quartz.nip51Lists.muteList.mutedWords -import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent class PeopleListDecryptionCache( val signer: NostrSigner, ) { - val cachedPrivateLists = PrivateTagArrayEventCache<PeopleListEvent>(signer) + val cachedPrivateLists = PrivateTagArrayEventCache<FollowSetEvent>(signer) - fun cachedUsersAndWords(event: PeopleListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUsersAndWords() + fun cachedUsersAndWords(event: FollowSetEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUsersAndWords() - fun cachedUsers(event: PeopleListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUsers() + fun cachedUsers(event: FollowSetEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUsers() - fun cachedUserIdSet(event: PeopleListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUserIdSet() + fun cachedUserIdSet(event: FollowSetEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUserIdSet() - fun cachedWords(event: PeopleListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedWords() + fun cachedWords(event: FollowSetEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedWords() - fun cachedWordSet(event: PeopleListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedWordSet() + fun cachedWordSet(event: FollowSetEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedWordSet() - suspend fun usersAndWords(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedUsersAndWords() + suspend fun usersAndWords(event: FollowSetEvent) = cachedPrivateLists.mergeTagList(event).mutedUsersAndWords() - suspend fun users(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedUsers() + suspend fun users(event: FollowSetEvent) = cachedPrivateLists.mergeTagList(event).mutedUsers() - suspend fun userIdSet(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedUserIdSet() + suspend fun userIdSet(event: FollowSetEvent) = cachedPrivateLists.mergeTagList(event).mutedUserIdSet() - suspend fun words(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedWords() + suspend fun words(event: FollowSetEvent) = cachedPrivateLists.mergeTagList(event).mutedWords() - suspend fun wordSet(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedWordSet() + suspend fun wordSet(event: FollowSetEvent) = cachedPrivateLists.mergeTagList(event).mutedWordSet() - suspend fun privateUsers(event: PeopleListEvent) = cachedPrivateLists.privateTags(event)?.mutedUsers() ?: emptyList() + suspend fun privateUsers(event: FollowSetEvent) = cachedPrivateLists.privateTags(event)?.mutedUsers() ?: emptyList() - suspend fun privateUserIdSet(event: PeopleListEvent) = cachedPrivateLists.privateTags(event)?.mutedUserIdSet() ?: emptySet() + suspend fun privateUserIdSet(event: FollowSetEvent) = cachedPrivateLists.privateTags(event)?.mutedUserIdSet() ?: emptySet() } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/relayFeeds/RelayFeedsListDecryptionCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/relayFeeds/RelayFeedsListDecryptionCache.kt index 3d262d28b9..694897a993 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/relayFeeds/RelayFeedsListDecryptionCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/relayFeeds/RelayFeedsListDecryptionCache.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.commons.model.nip51Lists.relayFeeds import com.vitorpamplona.amethyst.commons.model.nip51Lists.relayLists.GenericRelayListCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent class RelayFeedsListDecryptionCache( signer: NostrSigner, -) : GenericRelayListCache<RelayFeedsListEvent>(signer) +) : GenericRelayListCache<FavoriteRelayListEvent>(signer) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardDecryptionCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardDecryptionCache.kt index e6e5477791..61bb8a3705 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardDecryptionCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardDecryptionCache.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache -import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.users.petName import com.vitorpamplona.quartz.nip85TrustedAssertions.users.summary @@ -37,24 +37,24 @@ import com.vitorpamplona.quartz.nip85TrustedAssertions.users.summary class ContactCardDecryptionCache( val signer: NostrSigner, ) { - val cachedPrivateCards = PrivateTagArrayEventCache<ContactCardEvent>(signer, cacheSize = 100) + val cachedPrivateCards = PrivateTagArrayEventCache<UserAssertionEvent>(signer, cacheSize = 100) - suspend fun petName(event: ContactCardEvent) = cachedPrivateCards.mergeTagList(event).petName() + suspend fun petName(event: UserAssertionEvent) = cachedPrivateCards.mergeTagList(event).petName() - suspend fun summary(event: ContactCardEvent) = cachedPrivateCards.mergeTagList(event).summary() + suspend fun summary(event: UserAssertionEvent) = cachedPrivateCards.mergeTagList(event).summary() /** * The decrypted petname and summary plus the card's full decrypted tag list, * so renderers can resolve the NIP-30 `emoji` mappings stored alongside them. */ - suspend fun nickname(event: ContactCardEvent): Nickname? = cachedPrivateCards.mergeTagList(event).toNickname() + suspend fun nickname(event: UserAssertionEvent): Nickname? = cachedPrivateCards.mergeTagList(event).toNickname() /** * Synchronous variant that only reads an already-decrypted card, for use as * the immediate value of UI flows. Returns null until the suspend path has * decrypted the card once. */ - fun cachedNickname(event: ContactCardEvent): Nickname? = cachedPrivateCards.mergeTagListPrecached(event).toNickname() + fun cachedNickname(event: UserAssertionEvent): Nickname? = cachedPrivateCards.mergeTagListPrecached(event).toNickname() private fun TagArray.toNickname(): Nickname? { val name = petName() diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardsState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardsState.kt index f643429c5c..ce8635472b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardsState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardsState.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.IO @@ -59,11 +59,11 @@ class ContactCardsState( ) { private val accountUser: User? by lazy { cache.getOrCreateUser(signer.pubKey) } - fun createCardAddress(target: HexKey): Address = ContactCardEvent.createAddress(signer.pubKey, target) + fun createCardAddress(target: HexKey): Address = UserAssertionEvent.createAddress(signer.pubKey, target) fun getCardNote(target: HexKey): AddressableNote = cache.getOrCreateAddressableNote(createCardAddress(target)) - fun getCard(target: HexKey): ContactCardEvent? = getCardNote(target).event as? ContactCardEvent + fun getCard(target: HexKey): UserAssertionEvent? = getCardNote(target).event as? UserAssertionEvent /** * The account's own card about [target], as attached to the target user's @@ -71,7 +71,7 @@ class ContactCardsState( * per-user cache (like `metadata()` does) so a card arriving later is seen. */ @OptIn(ExperimentalCoroutinesApi::class) - fun myCardFlow(target: User): Flow<ContactCardEvent?> = + fun myCardFlow(target: User): Flow<UserAssertionEvent?> = target .cards() .receivedCards @@ -82,7 +82,7 @@ class ContactCardsState( ?.flow() ?.metadata ?.stateFlow - ?.map { it.note.event as? ContactCardEvent } + ?.map { it.note.event as? UserAssertionEvent } ?: flowOf(null) } @@ -110,7 +110,7 @@ class ContactCardsState( ?.receivedCards ?.value ?.get(accountUser) - ?.event as? ContactCardEvent ?: return null + ?.event as? UserAssertionEvent ?: return null return decryptionCache.cachedNickname(card) } @@ -145,12 +145,12 @@ class ContactCardsState( target: HexKey, petName: String?, summary: String?, - ): ContactCardEvent { + ): UserAssertionEvent { val emojis = emojiPacks.findEmojiTags(listOfNotNull(petName, summary).joinToString(" ")) val existing = getCard(target) return if (existing != null) { signer.sign( - ContactCardEvent.updatePetNameAndSummary( + UserAssertionEvent.updatePetNameAndSummary( earlierVersion = existing, petName = petName, summary = summary, @@ -159,7 +159,7 @@ class ContactCardsState( ), ) } else { - ContactCardEvent.create( + UserAssertionEvent.create( targetUser = target, petName = petName, summary = summary, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/UserCardsCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/UserCardsCache.kt index 5077850035..afe53a380b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/UserCardsCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/UserCardsCache.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.UserDependencies import com.vitorpamplona.amethyst.commons.relays.EOSERelayList import com.vitorpamplona.amethyst.commons.util.PlatformNumberFormatter -import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.flow.MutableStateFlow @@ -104,7 +104,7 @@ class UserCardsCache : UserDependencies { emit(null) } }.map { - (it?.note?.event as? ContactCardEvent)?.rank() + (it?.note?.event as? UserAssertionEvent)?.rank() }.flowOn(Dispatchers.IO) private val formatter = PlatformNumberFormatter() @@ -132,7 +132,7 @@ class UserCardsCache : UserDependencies { emit(null) } }.map { - val value = (it?.note?.event as? ContactCardEvent)?.followerCount() + val value = (it?.note?.event as? UserAssertionEvent)?.followerCount() if (value != null && value > 0) { formatter.format(value.toLong()) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/ChatPreview.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/ChatPreview.kt index aa3502cc0b..55d278e140 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/ChatPreview.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/ChatPreview.kt @@ -22,12 +22,12 @@ package com.vitorpamplona.amethyst.commons.model.privateChats import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip51Lists.PrivateReplaceableTagArrayEvent import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent /** @@ -44,11 +44,11 @@ import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent */ fun Event.hasEncryptedContent(): Boolean = when (this) { - is PrivateDmEvent -> true + is EncryptedDmEvent -> true is DraftWrapEvent -> true - is SealedRumorEvent -> true + is SealEvent -> true is GiftWrapEvent -> true - is LnZapRequestEvent -> isPrivateZap() + is ZapRequestEvent -> isPrivateZap() // Every NIP-51 list keeps its private members as a NIP-44 payload in `content`. The two // base classes cover all ~30 list kinds, present and future, which is the point: naming // them one at a time is how kind 30005 came to print its own ciphertext on screen. @@ -105,7 +105,7 @@ fun chatPreviewOf( if (!canDecrypt) return ChatPreview.Undecryptable // A kind:4 addressed to neither me nor from me can't be opened with my key, ever. - if (event is PrivateDmEvent && myPubKey != null && !event.isIncluded(myPubKey)) { + if (event is EncryptedDmEvent && myPubKey != null && !event.isIncluded(myPubKey)) { return ChatPreview.Undecryptable } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt index b197e44971..61c493964c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.amethyst.commons.util.WeakReference import com.vitorpamplona.amethyst.commons.util.withLock import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.paging.RelayLoadingCursors -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip14Subject.subject import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent import kotlinx.coroutines.channels.BufferOverflow @@ -151,8 +151,8 @@ class Chatroom : NotesGatherer { } else { // Old conversation, keep the last one. sorted.take(1).toSet() - } + sorted.filter { it.flowSet?.isInUse() ?: false } + sorted.filter { it.event !is PrivateDmEvent && it.event !is BaseDMGroupEvent } - // Both DM protocols are pruned by the recency rule above: NIP-04 (PrivateDmEvent) and NIP-17 + } + sorted.filter { it.flowSet?.isInUse() ?: false } + sorted.filter { it.event !is EncryptedDmEvent && it.event !is BaseDMGroupEvent } + // Both DM protocols are pruned by the recency rule above: NIP-04 (EncryptedDmEvent) and NIP-17 // (BaseDMGroupEvent rumors — ChatMessageEvent / file headers). Anything else that ever lands in a // room is kept. The caller realigns the per-relay download window for the dropped messages so // they can be paged again later (see LocalCache.pruneOldMessages + RelayLoadingCursors.rewindTo). diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/topNavFeeds/UsingRelayUnwrapper.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/topNavFeeds/UsingRelayUnwrapper.kt index 195c303fdb..c0824d52d2 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/topNavFeeds/UsingRelayUnwrapper.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/topNavFeeds/UsingRelayUnwrapper.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.utils.mapOfSet import kotlinx.coroutines.flow.Flow @@ -49,7 +49,7 @@ class UsingRelayUnwrapper { val relays = when (val noteEvent = outboxNote.note.event) { is AdvertisedRelayListEvent -> noteEvent.relaysNorm() - is ChatMessageRelayListEvent -> noteEvent.relays() + is DmRelayListEvent -> noteEvent.relays() else -> emptySet() } @@ -81,7 +81,7 @@ class UsingRelayUnwrapper { users .map { pubkeyHex -> cache - .getOrCreateAddressableNote(ChatMessageRelayListEvent.createAddress(pubkeyHex)) + .getOrCreateAddressableNote(DmRelayListEvent.createAddress(pubkeyHex)) .flow() .metadata.stateFlow.value }.toTypedArray() @@ -100,7 +100,7 @@ class UsingRelayUnwrapper { note.flow().metadata.stateFlow } + users.map { pubkeyHex -> - val note = cache.getOrCreateAddressableNote(ChatMessageRelayListEvent.createAddress(pubkeyHex)) + val note = cache.getOrCreateAddressableNote(DmRelayListEvent.createAddress(pubkeyHex)) note.flow().metadata.stateFlow } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/topNavFeeds/noteBased/NoteFeedFlow.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/topNavFeeds/noteBased/NoteFeedFlow.kt index eaf06a32b7..d48e5704a0 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/topNavFeeds/noteBased/NoteFeedFlow.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/topNavFeeds/noteBased/NoteFeedFlow.kt @@ -35,11 +35,11 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.Mute import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -61,8 +61,8 @@ class NoteFeedFlow( outboxRelays: Set<NormalizedRelayUrl>, ): IFeedTopNavFilter = when (noteEvent) { - is PeopleListEvent -> { - if (noteEvent.dTag() == PeopleListEvent.BLOCK_LIST_D_TAG) { + is FollowSetEvent -> { + if (noteEvent.dTag() == FollowSetEvent.BLOCK_LIST_D_TAG) { MutedAuthorsByOutboxTopNavFilter(caches.peopleListCache.cachedUserIdSet(noteEvent), blockedRelays) } else { AuthorsByOutboxTopNavFilter(caches.peopleListCache.cachedUserIdSet(noteEvent), blockedRelays) @@ -73,7 +73,7 @@ class NoteFeedFlow( MutedAuthorsByOutboxTopNavFilter(caches.muteListCache.cachedUserIdSet(noteEvent), blockedRelays) } - is FollowListEvent -> { + is StarterPackEvent -> { AuthorsByOutboxTopNavFilter(noteEvent.followIdSet(), blockedRelays) } @@ -81,7 +81,7 @@ class NoteFeedFlow( AllCommunitiesTopNavFilter(caches.communityListCache.cachedCommunityIdSet(noteEvent), blockedRelays) } - is HashtagListEvent -> { + is InterestListEvent -> { HashtagTopNavFilter(caches.hashtagCache.cachedHashtags(noteEvent), outboxRelays) } @@ -108,8 +108,8 @@ class NoteFeedFlow( outboxRelays: Set<NormalizedRelayUrl>, ) { when (noteEvent) { - is PeopleListEvent -> { - if (noteEvent.dTag() == PeopleListEvent.BLOCK_LIST_D_TAG) { + is FollowSetEvent -> { + if (noteEvent.dTag() == FollowSetEvent.BLOCK_LIST_D_TAG) { emit(MutedAuthorsByOutboxTopNavFilter(caches.peopleListCache.userIdSet(noteEvent), blockedRelays)) } else { emit(AuthorsByOutboxTopNavFilter(caches.peopleListCache.userIdSet(noteEvent), blockedRelays)) @@ -120,7 +120,7 @@ class NoteFeedFlow( emit(MutedAuthorsByOutboxTopNavFilter(caches.muteListCache.mutedUserIdSet(noteEvent), blockedRelays)) } - is FollowListEvent -> { + is StarterPackEvent -> { emit(AuthorsByOutboxTopNavFilter(noteEvent.followIdSet(), blockedRelays)) } @@ -128,7 +128,7 @@ class NoteFeedFlow( emit(AllCommunitiesTopNavFilter(caches.communityListCache.communityIdSet(noteEvent), blockedRelays)) } - is HashtagListEvent -> { + is InterestListEvent -> { emit(HashtagTopNavFilter(caches.hashtagCache.hashtags(noteEvent), outboxRelays)) } @@ -160,8 +160,8 @@ class NoteFeedFlow( proxyRelays: Set<NormalizedRelayUrl>, ): IFeedTopNavFilter = when (noteEvent) { - is PeopleListEvent -> { - if (noteEvent.dTag() == PeopleListEvent.BLOCK_LIST_D_TAG) { + is FollowSetEvent -> { + if (noteEvent.dTag() == FollowSetEvent.BLOCK_LIST_D_TAG) { MutedAuthorsByProxyTopNavFilter(caches.peopleListCache.cachedUserIdSet(noteEvent), proxyRelays) } else { AuthorsByProxyTopNavFilter(caches.peopleListCache.cachedUserIdSet(noteEvent), proxyRelays) @@ -172,7 +172,7 @@ class NoteFeedFlow( MutedAuthorsByProxyTopNavFilter(caches.muteListCache.cachedUserIdSet(noteEvent), proxyRelays) } - is FollowListEvent -> { + is StarterPackEvent -> { AuthorsByProxyTopNavFilter(noteEvent.followIdSet(), proxyRelays) } @@ -180,7 +180,7 @@ class NoteFeedFlow( AllCommunitiesTopNavFilter(caches.communityListCache.cachedCommunityIdSet(noteEvent), blockedRelays) } - is HashtagListEvent -> { + is InterestListEvent -> { HashtagTopNavFilter(caches.hashtagCache.cachedHashtags(noteEvent), proxyRelays) } @@ -207,8 +207,8 @@ class NoteFeedFlow( proxyRelays: Set<NormalizedRelayUrl>, ) { when (noteEvent) { - is PeopleListEvent -> { - if (noteEvent.dTag() == PeopleListEvent.BLOCK_LIST_D_TAG) { + is FollowSetEvent -> { + if (noteEvent.dTag() == FollowSetEvent.BLOCK_LIST_D_TAG) { emit(MutedAuthorsByProxyTopNavFilter(caches.peopleListCache.userIdSet(noteEvent), proxyRelays)) } else { emit(AuthorsByProxyTopNavFilter(caches.peopleListCache.userIdSet(noteEvent), proxyRelays)) @@ -219,7 +219,7 @@ class NoteFeedFlow( emit(MutedAuthorsByProxyTopNavFilter(caches.muteListCache.mutedUserIdSet(noteEvent), proxyRelays)) } - is FollowListEvent -> { + is StarterPackEvent -> { emit(AuthorsByProxyTopNavFilter(noteEvent.followIdSet(), proxyRelays)) } @@ -227,7 +227,7 @@ class NoteFeedFlow( emit(AllCommunitiesTopNavFilter(caches.communityListCache.communityIdSet(noteEvent), blockedRelays)) } - is HashtagListEvent -> { + is InterestListEvent -> { emit(HashtagTopNavFilter(caches.hashtagCache.hashtags(noteEvent), proxyRelays)) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/moderation/HashtagSpamCheck.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/moderation/HashtagSpamCheck.kt index d704e81e7a..e72e8201ee 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/moderation/HashtagSpamCheck.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/moderation/HashtagSpamCheck.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.commons.moderation import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasMoreHashtagsThan -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent /** * Pure decision: should this note be collapsed because it abuses hashtag `t` tags? @@ -46,7 +46,7 @@ object HashtagSpamCheck { ): Boolean { if (!enabled) return false if (displayedEvent == null) return false - if (displayedEvent.kind == LongTextNoteEvent.KIND) return false + if (displayedEvent.kind == LongFormContentEvent.KIND) return false if (authorPubkey != null && authorPubkey in exemptKeys) return false return displayedEvent.tags.hasMoreHashtagsThan(threshold) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/moderation/notifications/NotificationKinds.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/moderation/notifications/NotificationKinds.kt index 1d911cd5bc..1147d8eeaf 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/moderation/notifications/NotificationKinds.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/moderation/notifications/NotificationKinds.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.commons.moderation.notifications import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent @@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent @@ -60,7 +60,7 @@ object NotificationKinds { val SUBSCRIPTION_KINDS: List<Int> = listOf( TextNoteEvent.KIND, // 1 — mentions + replies - PrivateDmEvent.KIND, // 4 — NIP-04 legacy DM + EncryptedDmEvent.KIND, // 4 — NIP-04 legacy DM RepostEvent.KIND, // 6 ReactionEvent.KIND, // 7 ChatMessageEvent.KIND, // 14 — NIP-17 DM rumor (rarely arrives raw; gift-wrap is more common) @@ -69,7 +69,7 @@ object NotificationKinds { CommentEvent.KIND, // 1111 — NIP-22 threaded comment GiftWrapEvent.KIND, // 1059 — NIP-17 gift-wrapped DM NutzapEvent.KIND, // 9321 — NIP-61 Cashu nutzap - LnZapEvent.KIND, // 9735 — NIP-57 zap receipt + ZapReceiptEvent.KIND, // 9735 — NIP-57 zap receipt OnchainZapEvent.KIND, // 8333 — onchain zap Bolt12ZapEvent.KIND, // 9736 — NIP-B1 BOLT12 zap // NIP-17 file-header messages (encrypted file DMs) @@ -114,7 +114,7 @@ object NotificationKinds { // Own events never notify — except zap receipts (LnZap/Nutzap/Onchain) // which are signed by the LNURL provider or the payer, not by us. if (event.pubKey == myPubKeyHex && - event !is LnZapEvent && + event !is ZapReceiptEvent && event !is NutzapEvent && event !is OnchainZapEvent && event !is Bolt12ZapEvent diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletRelayCleartext.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletRelayCleartext.kt index 9cbee53bd1..8bd8d29085 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletRelayCleartext.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletRelayCleartext.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip44Encryption.Nip44v2 /** NAP-RELAY read boundary: encrypted event content is decrypted or withheld, never exposed. */ @@ -56,7 +56,7 @@ object NappletRelayCleartext { } fun isEncrypted(event: Event): Boolean = - event is PrivateDmEvent || + event is EncryptedDmEvent || EncryptedInfo.isNIP04(event.content) || isNip44V2(event.content) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterBasicAccountInfoFromKeys.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterBasicAccountInfoFromKeys.kt index 5244802c74..6026a0bba8 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterBasicAccountInfoFromKeys.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterBasicAccountInfoFromKeys.kt @@ -28,16 +28,16 @@ import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent import com.vitorpamplona.quartz.nip53LiveActivities.nestsServers.NestsServersEvent @@ -51,7 +51,7 @@ val BasicAccountInfoKinds = MetadataEvent.KIND, ContactListEvent.KIND, AdvertisedRelayListEvent.KIND, - ChatMessageRelayListEvent.KIND, + DmRelayListEvent.KIND, KeyPackageRelayListEvent.KIND, SearchRelayListEvent.KIND, FileServersEvent.KIND, @@ -66,10 +66,10 @@ val BasicAccountInfoKinds2 = BroadcastRelayListEvent.KIND, IndexerRelayListEvent.KIND, ProxyRelayListEvent.KIND, - HashtagListEvent.KIND, + InterestListEvent.KIND, GeohashListEvent.KIND, TrustProviderListEvent.KIND, - RelayFeedsListEvent.KIND, + FavoriteRelayListEvent.KIND, InterestSetEvent.KIND, // NIP-51 "simple groups" list (kind 10009), matching the sibling NIP-51 lists above. SimpleGroupListEvent.KIND, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterBookmarksAndReportsFromKey.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterBookmarksAndReportsFromKey.kt index 285fa83cb4..5d4a10c7a8 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterBookmarksAndReportsFromKey.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterBookmarksAndReportsFromKey.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent @@ -37,7 +37,7 @@ val ReportsAndBookmarksFromKeyKinds = ReportEvent.KIND, BookmarkListEvent.KIND, OldBookmarkListEvent.KIND, - LabeledBookmarkListEvent.KIND, + BookmarkSetEvent.KIND, PinListEvent.KIND, RequestToVanishEvent.KIND, ) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterFollowsAndMutesFromKey.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterFollowsAndMutesFromKey.kt index 1e9840ef13..fc0436f6a0 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterFollowsAndMutesFromKey.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterFollowsAndMutesFromKey.kt @@ -26,24 +26,24 @@ import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEve import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent val FollowAndMutesFromKeyKinds = listOf( - PeopleListEvent.KIND, - FollowListEvent.KIND, + FollowSetEvent.KIND, + StarterPackEvent.KIND, MuteListEvent.KIND, AcceptedBadgeSetEvent.KIND, ProfileBadgesEvent.KIND, - EmojiPackSelectionEvent.KIND, + EmojiListEvent.KIND, EphemeralChatListEvent.KIND, - ChannelListEvent.KIND, + PublicChatListEvent.KIND, ) fun filterFollowsAndMutesFromKey( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/nip01Notifications/FilterNotificationsToPubkey.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/nip01Notifications/FilterNotificationsToPubkey.kt index 9cdd3a11c6..a341e4f78c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/nip01Notifications/FilterNotificationsToPubkey.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/nip01Notifications/FilterNotificationsToPubkey.kt @@ -48,12 +48,12 @@ import com.vitorpamplona.quartz.nip34Git.status.GitStatusAppliedEvent import com.vitorpamplona.quartz.nip34Git.status.GitStatusClosedEvent import com.vitorpamplona.quartz.nip34Git.status.GitStatusDraftEvent import com.vitorpamplona.quartz.nip34Git.status.GitStatusOpenEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent import com.vitorpamplona.quartz.nip52Calendar.rsvp.CalendarRSVPEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent import com.vitorpamplona.quartz.nip64Chess.challenge.accept.LiveChessGameAcceptEvent import com.vitorpamplona.quartz.nip64Chess.move.LiveChessMoveEvent @@ -79,7 +79,7 @@ val GroupNotificationKinds = PollResponseEvent.KIND, RepostEvent.KIND, GenericRepostEvent.KIND, - LnZapEvent.KIND, + ZapReceiptEvent.KIND, Bolt12ZapEvent.KIND, ReportEvent.KIND, ) @@ -90,7 +90,7 @@ val SummaryKinds = ReactionEvent.KIND, RepostEvent.KIND, GenericRepostEvent.KIND, - LnZapEvent.KIND, + ZapReceiptEvent.KIND, OnchainZapEvent.KIND, Bolt12ZapEvent.KIND, ) @@ -98,7 +98,7 @@ val SummaryKinds = val NotificationsPerKeyKinds = listOf( ReportEvent.KIND, - LnZapPaymentResponseEvent.KIND, + NwcResponseEvent.KIND, ChannelMessageEvent.KIND, EphemeralChatEvent.KIND, BadgeAwardEvent.KIND, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/ContactCardFilters.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/ContactCardFilters.kt index 87df5fba9c..237c08b2f9 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/ContactCardFilters.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/ContactCardFilters.kt @@ -26,9 +26,9 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.dTag.DTag -import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent -val ContactCardKindList = listOf(ContactCardEvent.KIND) +val ContactCardKindList = listOf(UserAssertionEvent.KIND) /** * Kind:30382 cards *about* [targets], written by [trustedAccounts] (the account diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/calendars/CalendarKinds.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/calendars/CalendarKinds.kt index 80846f47b5..9842410406 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/calendars/CalendarKinds.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/calendars/CalendarKinds.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.commons.relayClient.calendars import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import com.vitorpamplona.quartz.nip52Calendar.rsvp.CalendarRSVPEvent // Appointments are the only kinds shown in the main calendar feed/views. RSVPs and @@ -34,6 +34,6 @@ val AllCalendarKinds = listOf( CalendarTimeSlotEvent.KIND, CalendarDateSlotEvent.KIND, - CalendarEvent.KIND, + CalendarCollectionEvent.KIND, CalendarRSVPEvent.KIND, ) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/FilterGoalForLiveActivity.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/FilterGoalForLiveActivity.kt index 66f6929113..2468f528f6 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/FilterGoalForLiveActivity.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/FilterGoalForLiveActivity.kt @@ -25,8 +25,8 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFil import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent @@ -50,7 +50,7 @@ fun filterGoalForLiveActivities( filter = ExplainedFilter( purpose = SubPurpose.LIVE_CHAT, - kinds = listOf(GoalEvent.KIND), + kinds = listOf(ZapGoalEvent.KIND), ids = listOf(goalId), limit = 1, ), @@ -60,7 +60,7 @@ fun filterGoalForLiveActivities( filter = ExplainedFilter( purpose = SubPurpose.LIVE_CHAT, - kinds = listOf(LnZapEvent.KIND, OnchainZapEvent.KIND, Bolt12ZapEvent.KIND), + kinds = listOf(ZapReceiptEvent.KIND, OnchainZapEvent.KIND, Bolt12ZapEvent.KIND), tags = mapOf("e" to listOf(goalId)), limit = 200, since = since?.get(relay)?.time, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/FilterMessagesToLiveStream.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/FilterMessagesToLiveStream.kt index a065038175..5ae7424389 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/FilterMessagesToLiveStream.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/FilterMessagesToLiveStream.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent import com.vitorpamplona.quartz.nip53LiveActivities.raid.LiveActivitiesRaidEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent @@ -47,7 +47,7 @@ fun filterMessagesToLiveActivities( LiveActivitiesChatMessageEvent.KIND, LiveActivitiesRaidEvent.KIND, LiveActivitiesClipEvent.KIND, - LnZapEvent.KIND, + ZapReceiptEvent.KIND, OnchainZapEvent.KIND, Bolt12ZapEvent.KIND, ), diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup/RelayGroupFilterBuilders.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup/RelayGroupFilterBuilders.kt index 94135ea335..3b3b5226f2 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup/RelayGroupFilterBuilders.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup/RelayGroupFilterBuilders.kt @@ -55,7 +55,7 @@ import com.vitorpamplona.quartz.buzz.workflow.WorkflowTriggeredEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip29RelayGroups.GroupId @@ -63,8 +63,8 @@ import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupAdminsEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMembersEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupPinnedEvent -import com.vitorpamplona.quartz.nip29RelayGroups.metadata.SupportedRolesEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.DeleteEventEvent +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupRolesEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupDeleteEventEvent import com.vitorpamplona.quartz.nip29RelayGroups.tags.GroupIdTag import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.GroupTag import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent @@ -89,7 +89,7 @@ val RELAY_GROUP_METADATA_KINDS = GroupMetadataEvent.KIND, GroupAdminsEvent.KIND, GroupMembersEvent.KIND, - SupportedRolesEvent.KIND, + GroupRolesEvent.KIND, ) /** @@ -224,9 +224,9 @@ val RELAY_GROUP_ALL_TIMELINE_KINDS = RELAY_GROUP_TIMELINE_KINDS + BUZZ_RELAY_GRO */ val RELAY_GROUP_AUX_KINDS = listOf( - DeletionEvent.KIND, + DeletionRequestEvent.KIND, ReactionEvent.KIND, - DeleteEventEvent.KIND, + GroupDeleteEventEvent.KIND, ) /** How many aux events a channel replays on subscribe. Bounds the backfill; live delivery is unbounded. */ diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/communities/FilterCommunityPosts.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/communities/FilterCommunityPosts.kt index ef795c5880..fa00fd2524 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/communities/FilterCommunityPosts.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/communities/FilterCommunityPosts.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent @@ -42,7 +42,7 @@ val CommunityPostKinds = RepostEvent.KIND, GenericRepostEvent.KIND, ClassifiedsEvent.KIND, - LongTextNoteEvent.KIND, + LongFormContentEvent.KIND, CommunityPostApprovalEvent.KIND, AttestationEvent.KIND, ) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/diagnostics/DmRelayDiagnosticsLogger.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/diagnostics/DmRelayDiagnosticsLogger.kt index f1ecab2c10..0eaa25ad91 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/diagnostics/DmRelayDiagnosticsLogger.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/diagnostics/DmRelayDiagnosticsLogger.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.AuthCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.utils.Log @@ -171,7 +171,7 @@ class DmRelayDiagnosticsLogger( // The kinds a DM-path REQ carries: NIP-17 gift wraps (1059 + 21059) and NIP-04 legacy DMs // (4). Matched exactly against the filter's "kinds" array — never as a substring of the whole // command, since a pubkey hex or timestamp can incidentally contain "1059" or "4". - private val DM_KINDS = setOf(GiftWrapEvent.KIND, EphemeralGiftWrapEvent.KIND, PrivateDmEvent.KIND) + private val DM_KINDS = setOf(GiftWrapEvent.KIND, EphemeralGiftWrapEvent.KIND, EncryptedDmEvent.KIND) private val KINDS_ARRAY = Regex("\"kinds\":\\[([0-9,\\s]*)]") diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormByAllCommunities.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormByAllCommunities.kt index ff5958375f..bd4233d7fa 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormByAllCommunities.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormByAllCommunities.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent fun filterLongFormAllCommunities( @@ -47,7 +47,7 @@ fun filterLongFormAllCommunities( tags = mapOf( "a" to communityList, - "k" to listOf(LongTextNoteEvent.KIND.toString()), + "k" to listOf(LongFormContentEvent.KIND.toString()), ), limit = 30, since = since, @@ -60,7 +60,7 @@ fun filterLongFormAllCommunities( ExplainedFilter( purpose = SubPurpose.DISCOVER_FEED, tags = mapOf("k" to listOf("5300"), "a" to communityList), - kinds = listOf(LongTextNoteEvent.KIND), + kinds = listOf(LongFormContentEvent.KIND), limit = 30, since = since, ), diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormByAuthors.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormByAuthors.kt index e9b4eab6ae..14275756c6 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormByAuthors.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormByAuthors.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent fun filterLongFormAuthors( relay: NormalizedRelayUrl, @@ -43,7 +43,7 @@ fun filterLongFormAuthors( ExplainedFilter( purpose = SubPurpose.DISCOVER_FEED, authors = authorList, - kinds = listOf(LongTextNoteEvent.KIND), + kinds = listOf(LongFormContentEvent.KIND), limit = 200, since = since, ), diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormByCommunity.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormByCommunity.kt index a93b476ab0..57a6f6a430 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormByCommunity.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormByCommunity.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent fun filterLongFormByCommunity( @@ -48,7 +48,7 @@ fun filterLongFormByCommunity( tags = mapOf( "a" to listOf(community), - "k" to listOf(LongTextNoteEvent.KIND.toString()), + "k" to listOf(LongFormContentEvent.KIND.toString()), ), limit = 30, since = since, @@ -62,7 +62,7 @@ fun filterLongFormByCommunity( purpose = SubPurpose.DISCOVER_FEED, authors = authors, tags = mapOf("k" to listOf("5300"), "a" to listOf(community)), - kinds = listOf(LongTextNoteEvent.KIND), + kinds = listOf(LongFormContentEvent.KIND), limit = 30, since = since, ), diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormByGeohash.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormByGeohash.kt index cd35d75baa..5b86dbcb4e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormByGeohash.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormByGeohash.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent fun filterLongFormByGeohash( relay: NormalizedRelayUrl, @@ -44,7 +44,7 @@ fun filterLongFormByGeohash( filter = ExplainedFilter( purpose = SubPurpose.DISCOVER_FEED, - kinds = listOf(LongTextNoteEvent.KIND), + kinds = listOf(LongFormContentEvent.KIND), tags = mapOf("g" to geoHashes), limit = 100, since = since, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormByHashtag.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormByHashtag.kt index e857cd1b62..adefda859e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormByHashtag.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormByHashtag.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent fun filterLongFormByHashtag( relay: NormalizedRelayUrl, @@ -44,7 +44,7 @@ fun filterLongFormByHashtag( filter = ExplainedFilter( purpose = SubPurpose.DISCOVER_FEED, - kinds = listOf(LongTextNoteEvent.KIND), + kinds = listOf(LongFormContentEvent.KIND), tags = mapOf("t" to hashtags), limit = 200, since = since, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormGlobal.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormGlobal.kt index 67b266a7ab..78219f6a18 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormGlobal.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip23LongForm/FilterLongFormGlobal.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFil import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.utils.TimeUtils fun filterLongFormGlobal( @@ -42,7 +42,7 @@ fun filterLongFormGlobal( filter = ExplainedFilter( purpose = SubPurpose.DISCOVER_FEED, - kinds = listOf(LongTextNoteEvent.KIND), + kinds = listOf(LongFormContentEvent.KIND), limit = 100, since = since, ), diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByAllCommunities.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByAllCommunities.kt index b59a5bec9c..714813a311 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByAllCommunities.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByAllCommunities.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent fun filterFollowSetsAllCommunities( @@ -48,7 +48,7 @@ fun filterFollowSetsAllCommunities( tags = mapOf( "a" to communityList, - "k" to listOf(FollowListEvent.KIND.toString()), + "k" to listOf(StarterPackEvent.KIND.toString()), ), limit = 300, since = since, @@ -61,7 +61,7 @@ fun filterFollowSetsAllCommunities( ExplainedFilter( purpose = SubPurpose.FOLLOW_LISTS, tags = mapOf("k" to listOf("5300"), "a" to communityList), - kinds = listOf(FollowListEvent.KIND), + kinds = listOf(StarterPackEvent.KIND), limit = 300, since = since, ), diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByAuthors.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByAuthors.kt index 0fc3c986d5..bcb5f3d944 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByAuthors.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByAuthors.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent fun filterFollowSetsAuthors( relay: NormalizedRelayUrl, @@ -43,7 +43,7 @@ fun filterFollowSetsAuthors( ExplainedFilter( purpose = SubPurpose.FOLLOW_LISTS, authors = authorList, - kinds = listOf(FollowListEvent.KIND), + kinds = listOf(StarterPackEvent.KIND), limit = 300, since = since, ), diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByCommunity.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByCommunity.kt index 095ab751ed..65483bc063 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByCommunity.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByCommunity.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent fun filterFollowSetsByCommunity( @@ -48,7 +48,7 @@ fun filterFollowSetsByCommunity( tags = mapOf( "a" to listOf(community), - "k" to listOf(FollowListEvent.KIND.toString()), + "k" to listOf(StarterPackEvent.KIND.toString()), ), limit = 300, since = since, @@ -62,7 +62,7 @@ fun filterFollowSetsByCommunity( purpose = SubPurpose.FOLLOW_LISTS, authors = authors, tags = mapOf("k" to listOf("5300"), "a" to listOf(community)), - kinds = listOf(FollowListEvent.KIND), + kinds = listOf(StarterPackEvent.KIND), limit = 300, since = since, ), diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByGeohash.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByGeohash.kt index f671ba36b7..8973d4358d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByGeohash.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByGeohash.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent fun filterFollowSetsByGeohash( relay: NormalizedRelayUrl, @@ -43,7 +43,7 @@ fun filterFollowSetsByGeohash( filter = ExplainedFilter( purpose = SubPurpose.FOLLOW_LISTS, - kinds = listOf(FollowListEvent.KIND), + kinds = listOf(StarterPackEvent.KIND), tags = mapOf("g" to geoHashes), limit = 300, since = since, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByHashtag.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByHashtag.kt index bc53534f50..21749a61e7 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByHashtag.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByHashtag.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent fun filterFollowSetsByHashtag( relay: NormalizedRelayUrl, @@ -44,7 +44,7 @@ fun filterFollowSetsByHashtag( filter = ExplainedFilter( purpose = SubPurpose.FOLLOW_LISTS, - kinds = listOf(FollowListEvent.KIND), + kinds = listOf(StarterPackEvent.KIND), tags = mapOf("t" to hashtags), limit = 300, since = since, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsGlobal.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsGlobal.kt index a853b22722..c62c7bf76e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsGlobal.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsGlobal.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFil import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent fun filterFollowSetsGlobal( relays: GlobalTopNavPerRelayFilterSet, @@ -41,7 +41,7 @@ fun filterFollowSetsGlobal( filter = ExplainedFilter( purpose = SubPurpose.FOLLOW_LISTS, - kinds = listOf(FollowListEvent.KIND), + kinds = listOf(StarterPackEvent.KIND), limit = 100, since = since, ), diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/watchers/FilterRepliesAndReactionsToAddresses.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/watchers/FilterRepliesAndReactionsToAddresses.kt index 25ad0322a1..0aadb128d6 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/watchers/FilterRepliesAndReactionsToAddresses.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/watchers/FilterRepliesAndReactionsToAddresses.kt @@ -29,7 +29,7 @@ import com.vitorpamplona.quartz.experimental.videoCollaboration.VideoCollaborati import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @@ -37,7 +37,7 @@ import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent @@ -51,7 +51,7 @@ val RepliesAndReactionsToAddressesKinds1 = RepostEvent.KIND, GenericRepostEvent.KIND, ReportEvent.KIND, - LnZapEvent.KIND, + ZapReceiptEvent.KIND, Bolt12ZapEvent.KIND, ZapPollEvent.KIND, CommentEvent.KIND, @@ -88,7 +88,7 @@ val RootScopedRepliesToAddressesKinds = val DeletionKindList = listOf( - DeletionEvent.KIND, + DeletionRequestEvent.KIND, ) val TextNoteKindList = listOf(TextNoteEvent.KIND) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/watchers/FilterRepliesAndReactionsToNotes.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/watchers/FilterRepliesAndReactionsToNotes.kt index 11c723a879..ea59b16521 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/watchers/FilterRepliesAndReactionsToNotes.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/event/watchers/FilterRepliesAndReactionsToNotes.kt @@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @@ -46,11 +46,11 @@ import com.vitorpamplona.quartz.nip34Git.status.GitStatusDraftEvent import com.vitorpamplona.quartz.nip34Git.status.GitStatusOpenEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.status.DvmStatusEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent import com.vitorpamplona.quartz.utils.mapOfSet @@ -62,7 +62,7 @@ val RepliesAndReactionsKinds = RepostEvent.KIND, GenericRepostEvent.KIND, ReportEvent.KIND, - LnZapEvent.KIND, + ZapReceiptEvent.KIND, OnchainZapEvent.KIND, Bolt12ZapEvent.KIND, OtsEvent.KIND, @@ -89,9 +89,9 @@ val RootScopedRepliesKinds = val RepliesAndReactionsKinds2 = listOf( - DeletionEvent.KIND, - NIP90ContentDiscoveryResponseEvent.KIND, - NIP90StatusEvent.KIND, + DeletionRequestEvent.KIND, + DvmContentDiscoveryResponseEvent.KIND, + DvmStatusEvent.KIND, TorrentCommentEvent.KIND, GitReplyEvent.KIND, // NIP-34 status events (1630/1631/1632/1633). Rooted at the target diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/geohash/FilterPostsByGeohash.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/geohash/FilterPostsByGeohash.kt index 102d73791a..63bf3ed5bc 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/geohash/FilterPostsByGeohash.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/geohash/FilterPostsByGeohash.kt @@ -34,9 +34,9 @@ import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent @@ -49,7 +49,7 @@ val PostsByGeohashKinds = listOf( TextNoteEvent.KIND, ChannelMessageEvent.KIND, - LongTextNoteEvent.KIND, + LongFormContentEvent.KIND, PollEvent.KIND, ZapPollEvent.KIND, ClassifiedsEvent.KIND, @@ -60,7 +60,7 @@ val PostsByGeohashKinds = MusicPlaylistEvent.KIND, PodcastEpisodeEvent.KIND, PodcastMetadataEvent.KIND, - WikiNoteEvent.KIND, + WikiArticleEvent.KIND, RoadEventReportEvent.KIND, RoadEventConfirmationEvent.KIND, GeocacheListingEvent.KIND, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip01Core/FilterHomePostsByGeohashes.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip01Core/FilterHomePostsByGeohashes.kt index 90bdc83585..ed72bcacfb 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip01Core/FilterHomePostsByGeohashes.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip01Core/FilterHomePostsByGeohashes.kt @@ -33,13 +33,13 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent @@ -49,10 +49,10 @@ val HomePostsByGeohashKinds = TextNoteEvent.KIND, RepostEvent.KIND, GenericRepostEvent.KIND, - LongTextNoteEvent.KIND, + LongFormContentEvent.KIND, ClassifiedsEvent.KIND, HighlightEvent.KIND, - WikiNoteEvent.KIND, + WikiArticleEvent.KIND, CommentEvent.KIND, VoiceEvent.KIND, AttestationEvent.KIND, @@ -60,8 +60,8 @@ val HomePostsByGeohashKinds = PictureEvent.KIND, VideoNormalEvent.KIND, VideoShortEvent.KIND, - VideoHorizontalEvent.KIND, - VideoVerticalEvent.KIND, + AddressableNormalVideoEvent.KIND, + AddressableShortVideoEvent.KIND, ) fun filterHomePostsByGeohashes( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip01Core/FilterHomePostsByHashtags.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip01Core/FilterHomePostsByHashtags.kt index 6983457805..ee76335ea9 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip01Core/FilterHomePostsByHashtags.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip01Core/FilterHomePostsByHashtags.kt @@ -36,13 +36,13 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent @@ -52,12 +52,12 @@ val HomePostsBuHashtagsKinds = TextNoteEvent.KIND, RepostEvent.KIND, GenericRepostEvent.KIND, - LongTextNoteEvent.KIND, + LongFormContentEvent.KIND, ClassifiedsEvent.KIND, HighlightEvent.KIND, AudioHeaderEvent.KIND, CommentEvent.KIND, - WikiNoteEvent.KIND, + WikiArticleEvent.KIND, NipTextEvent.KIND, VoiceEvent.KIND, AttestationEvent.KIND, @@ -65,8 +65,8 @@ val HomePostsBuHashtagsKinds = PictureEvent.KIND, VideoNormalEvent.KIND, VideoShortEvent.KIND, - VideoHorizontalEvent.KIND, - VideoVerticalEvent.KIND, + AddressableNormalVideoEvent.KIND, + AddressableShortVideoEvent.KIND, ) fun filterHomePostsByHashtags( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip65Follows/FilterHomePostsByAuthors.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip65Follows/FilterHomePostsByAuthors.kt index d7daaa94c4..e9d27d4d9f 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip65Follows/FilterHomePostsByAuthors.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip65Follows/FilterHomePostsByAuthors.kt @@ -40,19 +40,19 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip64Chess.challenge.offer.LiveChessGameChallengeEvent import com.vitorpamplona.quartz.nip64Chess.end.LiveChessGameEndEvent import com.vitorpamplona.quartz.nip64Chess.game.ChessGameEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent @@ -67,18 +67,18 @@ val HomePostsNewThreadKinds1 = RepostEvent.KIND, GenericRepostEvent.KIND, ClassifiedsEvent.KIND, - LongTextNoteEvent.KIND, + LongFormContentEvent.KIND, HighlightEvent.KIND, ZapPollEvent.KIND, PollEvent.KIND, - WikiNoteEvent.KIND, + WikiArticleEvent.KIND, AttestationEvent.KIND, NipTextEvent.KIND, PictureEvent.KIND, VideoNormalEvent.KIND, VideoShortEvent.KIND, - VideoHorizontalEvent.KIND, - VideoVerticalEvent.KIND, + AddressableNormalVideoEvent.KIND, + AddressableShortVideoEvent.KIND, ) val HomePostsNewThreadKinds2 = diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip72Communities/FilterHomePostsFromCommunities.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip72Communities/FilterHomePostsFromCommunities.kt index 096432af97..64e9a2f66d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip72Communities/FilterHomePostsFromCommunities.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip72Communities/FilterHomePostsFromCommunities.kt @@ -30,13 +30,13 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent @@ -45,11 +45,11 @@ import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent val HomePostsFromCommunityKinds = listOf( TextNoteEvent.KIND, - LongTextNoteEvent.KIND, + LongFormContentEvent.KIND, ClassifiedsEvent.KIND, HighlightEvent.KIND, PollEvent.KIND, - WikiNoteEvent.KIND, + WikiArticleEvent.KIND, NipTextEvent.KIND, CommunityPostApprovalEvent.KIND, CommentEvent.KIND, @@ -57,8 +57,8 @@ val HomePostsFromCommunityKinds = PictureEvent.KIND, VideoNormalEvent.KIND, VideoShortEvent.KIND, - VideoHorizontalEvent.KIND, - VideoVerticalEvent.KIND, + AddressableNormalVideoEvent.KIND, + AddressableShortVideoEvent.KIND, ) /** The same set, as the `k` tag values a NIP-72 post approval carries. */ diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip90AlgoFeeds/FilterHomePostsByAlgoFeedIds.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip90AlgoFeeds/FilterHomePostsByAlgoFeedIds.kt index d8dbc554ab..6f575ea96d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip90AlgoFeeds/FilterHomePostsByAlgoFeedIds.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip90AlgoFeeds/FilterHomePostsByAlgoFeedIds.kt @@ -28,8 +28,8 @@ import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.status.DvmStatusEvent /** * Builds relay REQ filters for a favorite-DVM home feed. @@ -112,8 +112,8 @@ private fun responseListenFilter( purpose = SubPurpose.HOME_FEED, kinds = listOf( - NIP90ContentDiscoveryResponseEvent.KIND, - NIP90StatusEvent.KIND, + DvmContentDiscoveryResponseEvent.KIND, + DvmStatusEvent.KIND, ), tags = mapOf("e" to requestIds), limit = 10 * requestIds.size, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/longs/FilterLongsByAllCommunities.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/longs/FilterLongsByAllCommunities.kt index c8b32bae95..f6461946c2 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/longs/FilterLongsByAllCommunities.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/longs/FilterLongsByAllCommunities.kt @@ -27,20 +27,20 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent val LongsFromCommunityKinds = listOf( VideoNormalEvent.KIND, - VideoHorizontalEvent.KIND, + AddressableNormalVideoEvent.KIND, ) val LongsFromCommunityKindsStr = listOf( VideoNormalEvent.KIND.toString(), - VideoHorizontalEvent.KIND.toString(), + AddressableNormalVideoEvent.KIND.toString(), ) fun filterLongsFromAllCommunities( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/longs/FilterLongsByAuthors.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/longs/FilterLongsByAuthors.kt index 2c365f4fbd..179349b97b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/longs/FilterLongsByAuthors.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/longs/FilterLongsByAuthors.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent fun filterLongsByAuthors( @@ -44,7 +44,7 @@ fun filterLongsByAuthors( ExplainedFilter( purpose = SubPurpose.MEDIA_FEED, authors = authorList, - kinds = listOf(VideoNormalEvent.KIND, VideoHorizontalEvent.KIND), + kinds = listOf(VideoNormalEvent.KIND, AddressableNormalVideoEvent.KIND), limit = 200, since = since, ), diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/longs/FilterLongsByGeohashes.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/longs/FilterLongsByGeohashes.kt index 8cf5096e02..9c0d605be8 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/longs/FilterLongsByGeohashes.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/longs/FilterLongsByGeohashes.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent fun filterLongsByGeohashes( @@ -42,7 +42,7 @@ fun filterLongsByGeohashes( filter = ExplainedFilter( purpose = SubPurpose.MEDIA_FEED, - kinds = listOf(VideoNormalEvent.KIND, VideoHorizontalEvent.KIND), + kinds = listOf(VideoNormalEvent.KIND, AddressableNormalVideoEvent.KIND), tags = mapOf("g" to geotags.sorted()), limit = 100, since = since, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/longs/FilterLongsByHashtag.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/longs/FilterLongsByHashtag.kt index 85e2eee01b..956bd69436 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/longs/FilterLongsByHashtag.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/longs/FilterLongsByHashtag.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent fun filterLongsByHashtag( @@ -40,7 +40,7 @@ fun filterLongsByHashtag( filter = ExplainedFilter( purpose = SubPurpose.MEDIA_FEED, - kinds = listOf(VideoNormalEvent.KIND, VideoHorizontalEvent.KIND), + kinds = listOf(VideoNormalEvent.KIND, AddressableNormalVideoEvent.KIND), tags = mapOf("t" to hashtags.toList()), limit = 200, since = since, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/longs/FilterLongsGlobal.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/longs/FilterLongsGlobal.kt index c538f86362..e9378496a7 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/longs/FilterLongsGlobal.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/longs/FilterLongsGlobal.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFil import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -43,7 +43,7 @@ fun filterLongsGlobal( filter = ExplainedFilter( purpose = SubPurpose.MEDIA_FEED, - kinds = listOf(VideoNormalEvent.KIND, VideoHorizontalEvent.KIND), + kinds = listOf(VideoNormalEvent.KIND, AddressableNormalVideoEvent.KIND), limit = 200, since = since, ), diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/nip17Dm/GiftWrapDecryptor.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/nip17Dm/GiftWrapDecryptor.kt index cf19417ca5..4b9d7a8bc4 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/nip17Dm/GiftWrapDecryptor.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/nip17Dm/GiftWrapDecryptor.kt @@ -22,25 +22,25 @@ package com.vitorpamplona.amethyst.commons.relayClient.nip17Dm import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent /** * Fully decrypt a NIP-59 gift wrap down to the inner rumor. * * A gift wrap carries two encryption layers: - * kind:1059 [GiftWrapEvent] → kind:13 [SealedRumorEvent] → the rumor. + * kind:1059 [GiftWrapEvent] → kind:13 [SealEvent] → the rumor. * * [GiftWrapEvent.unwrapOrNull] peels only the outer layer. Every non-Android * consumer that looks at the rumor directly (the CLI's `dm list`, the desktop * chat receive path, the Marmot welcome ingest in commons) needs both peels. * - * If the inner content isn't a [SealedRumorEvent] (malformed, or a future + * If the inner content isn't a [SealEvent] (malformed, or a future * NIP-59 variant that wraps a rumor directly), the outer unwrap result is * returned as-is so callers can still route on kind. Either unwrap returning * null propagates as null. */ suspend fun GiftWrapEvent.unwrapAndUnsealOrNull(signer: NostrSigner): Event? { val inner = unwrapOrNull(signer) ?: return null - return if (inner is SealedRumorEvent) inner.unsealOrNull(signer) else inner + return if (inner is SealEvent) inner.unsealOrNull(signer) else inner } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/nip47WalletConnect/FilterNWCPaymentsFromRequests.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/nip47WalletConnect/FilterNWCPaymentsFromRequests.kt index 815746628b..b4a468d7b2 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/nip47WalletConnect/FilterNWCPaymentsFromRequests.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/nip47WalletConnect/FilterNWCPaymentsFromRequests.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.commons.relayClient.nip47WalletConnect import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent // The request event id (#e) is a unique 32-byte identifier — sufficient on // its own to match a spec-compliant kind-23195 response. We deliberately keep @@ -43,7 +43,7 @@ fun filterNWCPaymentsFromRequests( fromUsers: Set<HexKey>, ): Filter = Filter( - kinds = listOf(LnZapPaymentResponseEvent.KIND), + kinds = listOf(NwcResponseEvent.KIND), tags = mapOf( "e" to paymentRequests.sorted(), diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfileLists.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfileLists.kt index 022f5cb712..1ebb50c63b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfileLists.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfileLists.kt @@ -29,9 +29,9 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent val UserProfileListKinds = @@ -39,9 +39,9 @@ val UserProfileListKinds = BookmarkListEvent.KIND, OldBookmarkListEvent.KIND, PinListEvent.KIND, - PeopleListEvent.KIND, - FollowListEvent.KIND, - HashtagListEvent.KIND, + FollowSetEvent.KIND, + StarterPackEvent.KIND, + InterestListEvent.KIND, AppRecommendationEvent.KIND, PaymentTargetsEvent.KIND, ) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfileMedia.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfileMedia.kt index 5d689fbb15..46d26ab82c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfileMedia.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfileMedia.kt @@ -29,17 +29,17 @@ import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryE import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent val UserProfileMediaKinds = listOf( PictureEvent.KIND, ProfileGalleryEntryEvent.KIND, - VideoVerticalEvent.KIND, - VideoHorizontalEvent.KIND, + AddressableShortVideoEvent.KIND, + AddressableNormalVideoEvent.KIND, VideoNormalEvent.KIND, VideoShortEvent.KIND, ) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfilePosts.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfilePosts.kt index a00b67bce8..ef9fe01388 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfilePosts.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfilePosts.kt @@ -40,11 +40,11 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent @@ -60,10 +60,10 @@ val UserProfilePostKinds1 = TextNoteEvent.KIND, GenericRepostEvent.KIND, RepostEvent.KIND, - LongTextNoteEvent.KIND, + LongFormContentEvent.KIND, PollEvent.KIND, HighlightEvent.KIND, - WikiNoteEvent.KIND, + WikiArticleEvent.KIND, VoiceEvent.KIND, PublicMessageEvent.KIND, ) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfileZapReceived.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfileZapReceived.kt index 8d5ab4790b..af26c39476 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfileZapReceived.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfileZapReceived.kt @@ -26,11 +26,11 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFil import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent -val UserProfileZapReceiverKinds = listOf(LnZapEvent.KIND, OnchainZapEvent.KIND, Bolt12ZapEvent.KIND) +val UserProfileZapReceiverKinds = listOf(ZapReceiptEvent.KIND, OnchainZapEvent.KIND, Bolt12ZapEvent.KIND) fun filterUserProfileZapsReceived( cache: ICacheProvider, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/relay/FilterPostsByRelay.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/relay/FilterPostsByRelay.kt index becd8b7779..a6fc642406 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/relay/FilterPostsByRelay.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/relay/FilterPostsByRelay.kt @@ -34,10 +34,10 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent @@ -48,12 +48,12 @@ val PostsByRelayKinds = listOf( TextNoteEvent.KIND, ChannelMessageEvent.KIND, - LongTextNoteEvent.KIND, + LongFormContentEvent.KIND, PollEvent.KIND, LiveActivitiesChatMessageEvent.KIND, ClassifiedsEvent.KIND, HighlightEvent.KIND, - WikiNoteEvent.KIND, + WikiArticleEvent.KIND, CommentEvent.KIND, ) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/shorts/FilterShortsByAllCommunities.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/shorts/FilterShortsByAllCommunities.kt index 18bcd8b0fa..9d420b8e6d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/shorts/FilterShortsByAllCommunities.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/shorts/FilterShortsByAllCommunities.kt @@ -27,20 +27,20 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent val ShortsFromCommunityKinds = listOf( VideoShortEvent.KIND, - VideoVerticalEvent.KIND, + AddressableShortVideoEvent.KIND, ) val ShortsFromCommunityKindsStr = listOf( VideoShortEvent.KIND.toString(), - VideoVerticalEvent.KIND.toString(), + AddressableShortVideoEvent.KIND.toString(), ) fun filterShortsFromAllCommunities( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/shorts/FilterShortsByAuthors.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/shorts/FilterShortsByAuthors.kt index 24bca3c574..e671e6a37f 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/shorts/FilterShortsByAuthors.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/shorts/FilterShortsByAuthors.kt @@ -28,8 +28,8 @@ import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent fun filterShortsByAuthors( relay: NormalizedRelayUrl, @@ -44,7 +44,7 @@ fun filterShortsByAuthors( ExplainedFilter( purpose = SubPurpose.MEDIA_FEED, authors = authorList, - kinds = listOf(VideoShortEvent.KIND, VideoVerticalEvent.KIND), + kinds = listOf(VideoShortEvent.KIND, AddressableShortVideoEvent.KIND), limit = 200, since = since, ), diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/shorts/FilterShortsByGeohashes.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/shorts/FilterShortsByGeohashes.kt index c9afffb742..36345aa08e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/shorts/FilterShortsByGeohashes.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/shorts/FilterShortsByGeohashes.kt @@ -26,8 +26,8 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent fun filterShortsByGeohashes( relay: NormalizedRelayUrl, @@ -42,7 +42,7 @@ fun filterShortsByGeohashes( filter = ExplainedFilter( purpose = SubPurpose.MEDIA_FEED, - kinds = listOf(VideoShortEvent.KIND, VideoVerticalEvent.KIND), + kinds = listOf(VideoShortEvent.KIND, AddressableShortVideoEvent.KIND), tags = mapOf("g" to geotags.sorted()), limit = 100, since = since, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/shorts/FilterShortsByHashtag.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/shorts/FilterShortsByHashtag.kt index dfe7382fe8..16cb6c6dcb 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/shorts/FilterShortsByHashtag.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/shorts/FilterShortsByHashtag.kt @@ -26,8 +26,8 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent fun filterShortsByHashtag( relay: NormalizedRelayUrl, @@ -40,7 +40,7 @@ fun filterShortsByHashtag( filter = ExplainedFilter( purpose = SubPurpose.MEDIA_FEED, - kinds = listOf(VideoShortEvent.KIND, VideoVerticalEvent.KIND), + kinds = listOf(VideoShortEvent.KIND, AddressableShortVideoEvent.KIND), tags = mapOf("t" to hashtags.toList()), limit = 200, since = since, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/shorts/FilterShortsGlobal.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/shorts/FilterShortsGlobal.kt index ee0c14a2b1..ece2a170e2 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/shorts/FilterShortsGlobal.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/shorts/FilterShortsGlobal.kt @@ -25,8 +25,8 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFil import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.utils.TimeUtils fun filterShortsGlobal( @@ -43,7 +43,7 @@ fun filterShortsGlobal( filter = ExplainedFilter( purpose = SubPurpose.MEDIA_FEED, - kinds = listOf(VideoShortEvent.KIND, VideoVerticalEvent.KIND), + kinds = listOf(VideoShortEvent.KIND, AddressableShortVideoEvent.KIND), limit = 200, since = since, ), diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/softwareapps/FilterSoftwareAppsByAuthors.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/softwareapps/FilterSoftwareAppsByAuthors.kt index 5eda45cb9b..1483c10824 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/softwareapps/FilterSoftwareAppsByAuthors.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/softwareapps/FilterSoftwareAppsByAuthors.kt @@ -26,10 +26,10 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFil import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent -import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.SoftwareReleaseEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent fun filterSoftwareAppsByAuthors( relay: NormalizedRelayUrl, @@ -44,7 +44,7 @@ fun filterSoftwareAppsByAuthors( ExplainedFilter( purpose = SubPurpose.ADD_ONS, authors = authorList, - kinds = listOf(SoftwareApplicationEvent.KIND, SoftwareReleaseEvent.KIND), + kinds = listOf(SoftwareApplicationEvent.KIND, ReleaseArtifactSetEvent.KIND), limit = 200, since = since, ), diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/softwareapps/FilterSoftwareAppsByHashtag.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/softwareapps/FilterSoftwareAppsByHashtag.kt index 638eee590e..00ff7c3723 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/softwareapps/FilterSoftwareAppsByHashtag.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/softwareapps/FilterSoftwareAppsByHashtag.kt @@ -25,10 +25,10 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFil import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent -import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.SoftwareReleaseEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts +import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent fun filterSoftwareAppsByHashtag( relay: NormalizedRelayUrl, @@ -45,7 +45,7 @@ fun filterSoftwareAppsByHashtag( filter = ExplainedFilter( purpose = SubPurpose.TAG_FEED, - kinds = listOf(SoftwareApplicationEvent.KIND, SoftwareReleaseEvent.KIND), + kinds = listOf(SoftwareApplicationEvent.KIND, ReleaseArtifactSetEvent.KIND), tags = mapOf("t" to expanded), limit = 200, since = since, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/softwareapps/FilterSoftwareAppsGlobal.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/softwareapps/FilterSoftwareAppsGlobal.kt index ed54dbf12d..c72a5c6ac1 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/softwareapps/FilterSoftwareAppsGlobal.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/softwareapps/FilterSoftwareAppsGlobal.kt @@ -25,10 +25,10 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFil import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent -import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.SoftwareReleaseEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl +import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent // zapstore's relay indexes the entire software-app catalog (NIP-82), so the // global Apps feed always queries it in addition to the user's own relays — @@ -57,7 +57,7 @@ fun filterSoftwareAppsGlobal( filter = ExplainedFilter( purpose = SubPurpose.ADD_ONS, - kinds = listOf(SoftwareApplicationEvent.KIND, SoftwareReleaseEvent.KIND), + kinds = listOf(SoftwareApplicationEvent.KIND, ReleaseArtifactSetEvent.KIND), // Keep the limit < 100. Relays such as zapstore's score filter // specificity and treat a limit >= 100 (or none) as too vague, // rejecting the whole REQ with "filters are too vague". diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/FilterUserMetadataForKey.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/FilterUserMetadataForKey.kt index db874ab832..0d1290113d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/FilterUserMetadataForKey.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/FilterUserMetadataForKey.kt @@ -32,8 +32,8 @@ import com.vitorpamplona.quartz.nip01Core.hints.HintIndexer import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent -import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent +import com.vitorpamplona.quartz.nip38UserStatus.UserStatusEvent import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent @@ -44,9 +44,9 @@ val UserMetadataForKeyKinds = listOf( MetadataEvent.KIND, ExternalIdentitiesEvent.KIND, - StatusEvent.KIND, + UserStatusEvent.KIND, AdvertisedRelayListEvent.KIND, - ChatMessageRelayListEvent.KIND, + DmRelayListEvent.KIND, KeyPackageRelayListEvent.KIND, PaymentTargetsEvent.KIND, Bolt12OfferListEvent.KIND, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/video/FeedBasis.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/video/FeedBasis.kt index a6d9bd3de3..17c4b31bdb 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/video/FeedBasis.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/video/FeedBasis.kt @@ -22,11 +22,11 @@ package com.vitorpamplona.amethyst.commons.relayClient.video import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent // The media types the video/shorts feeds will admit. HLS playlists are in here because a // blossom-hosted manifest is `https://host/<sha256>` with no extension at all, so the MIME is the @@ -57,8 +57,8 @@ val SUPPORTED_VIDEO_FEED_MIME_TYPES_SET = SUPPORTED_VIDEO_FEED_MIME_TYPES.toSet( val PictureAndVideoKinds = listOf( PictureEvent.KIND, - VideoHorizontalEvent.KIND, - VideoVerticalEvent.KIND, + AddressableNormalVideoEvent.KIND, + AddressableShortVideoEvent.KIND, VideoNormalEvent.KIND, VideoShortEvent.KIND, ) @@ -66,12 +66,12 @@ val PictureAndVideoKinds = val PictureAndVideoKTags = listOf( PictureEvent.KIND.toString(), - VideoHorizontalEvent.KIND.toString(), - VideoVerticalEvent.KIND.toString(), + AddressableNormalVideoEvent.KIND.toString(), + AddressableShortVideoEvent.KIND.toString(), VideoNormalEvent.KIND.toString(), VideoShortEvent.KIND.toString(), ) -val PictureAndVideoLegacyKinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND) -val PictureAndVideoLegacyKTags = listOf(FileHeaderEvent.KIND.toString(), FileStorageHeaderEvent.KIND.toString()) +val PictureAndVideoLegacyKinds = listOf(FileMetadataEvent.KIND, FileStorageHeaderEvent.KIND) +val PictureAndVideoLegacyKTags = listOf(FileMetadataEvent.KIND.toString(), FileStorageHeaderEvent.KIND.toString()) val LegacyMimeTypes = SUPPORTED_VIDEO_FEED_MIME_TYPES val LegacyMimeTypeMap = mapOf("m" to LegacyMimeTypes) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/video/nip72Communities/FilterPictureAndVideoByAllCommunities.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/video/nip72Communities/FilterPictureAndVideoByAllCommunities.kt index 530b8a2d2c..81bebc33d5 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/video/nip72Communities/FilterPictureAndVideoByAllCommunities.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/video/nip72Communities/FilterPictureAndVideoByAllCommunities.kt @@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent fun filterPictureAndVideoAllCommunities( relay: NormalizedRelayUrl, @@ -80,7 +80,7 @@ fun filterPictureAndVideoAllCommunities( tags = mapOf( "a" to communityList, - "k" to listOf(FileHeaderEvent.KIND.toString(), FileStorageHeaderEvent.KIND.toString()), + "k" to listOf(FileMetadataEvent.KIND.toString(), FileStorageHeaderEvent.KIND.toString()), ), limit = 200, since = since, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/KindRegistry.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/KindRegistry.kt index 6a61907b8c..34b7ce090f 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/KindRegistry.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/KindRegistry.kt @@ -27,29 +27,29 @@ import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent import com.vitorpamplona.quartz.nip5dNapplets.RootNappletEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent @@ -93,7 +93,7 @@ object KindRegistry { val aliases: Map<String, List<Int>> = mapOf( "note" to listOf(TextNoteEvent.KIND), - "article" to listOf(LongTextNoteEvent.KIND), + "article" to listOf(LongFormContentEvent.KIND), // No `repost` or `profile` here on purpose: both name kinds in // [RenderableKinds.NEVER_IN_RESULTS], so the chip drew and the list stayed empty // forever. `kind:profile` was the worse of the two — it pinned the scope to Notes, @@ -101,7 +101,7 @@ object KindRegistry { "channel" to listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND), "live" to listOf(LiveActivitiesEvent.KIND, MeetingSpaceEvent.KIND, MeetingRoomEvent.KIND), "community" to listOf(CommunityDefinitionEvent.KIND), - "wiki" to listOf(WikiNoteEvent.KIND), + "wiki" to listOf(WikiArticleEvent.KIND), "classified" to listOf(ClassifiedsEvent.KIND), "geocache" to listOf(GeocacheListingEvent.KIND), "highlight" to listOf(HighlightEvent.KIND), @@ -109,7 +109,7 @@ object KindRegistry { // The kinds the app's own feeds are windows onto, so a screen that seeds its search // gets a chip a reader recognises instead of a bare number. "picture" to listOf(PictureEvent.KIND), - "video" to listOf(VideoNormalEvent.KIND, VideoShortEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND), + "video" to listOf(VideoNormalEvent.KIND, VideoShortEvent.KIND, AddressableNormalVideoEvent.KIND, AddressableShortVideoEvent.KIND), "nsite" to listOf(RootSiteEvent.KIND, NamedSiteEvent.KIND), "napplet" to listOf(RootNappletEvent.KIND, NamedNappletEvent.KIND), "emoji" to listOf(EmojiPackEvent.KIND), @@ -123,15 +123,15 @@ object KindRegistry { "podcast" to listOf(PodcastMetadataEvent.KIND), "episode" to listOf(Podcasting20EpisodeEvent.KIND), "software" to listOf(SoftwareApplicationEvent.KIND), - "followpack" to listOf(FollowListEvent.KIND), + "followpack" to listOf(StarterPackEvent.KIND), "zappoll" to listOf(ZapPollEvent.KIND), // `video` is every video kind; these name the two the app gives their own feeds. - "stories" to listOf(VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND), - "short" to listOf(VideoVerticalEvent.KIND), - "longvideo" to listOf(VideoHorizontalEvent.KIND), + "stories" to listOf(AddressableNormalVideoEvent.KIND, AddressableShortVideoEvent.KIND), + "short" to listOf(AddressableShortVideoEvent.KIND), + "longvideo" to listOf(AddressableNormalVideoEvent.KIND), // The slots on a calendar, and the calendars that collect them. "calendar" to listOf(CalendarTimeSlotEvent.KIND, CalendarDateSlotEvent.KIND), - "calendarset" to listOf(CalendarEvent.KIND), + "calendarset" to listOf(CalendarCollectionEvent.KIND), ) val pseudoKinds: Set<String> = setOf("reply", "media") @@ -148,11 +148,11 @@ object KindRegistry { val presets: Map<String, ContentPreset> = mapOf( "Notes" to ContentPreset(kinds = listOf(TextNoteEvent.KIND)), - "Articles" to ContentPreset(kinds = listOf(LongTextNoteEvent.KIND)), + "Articles" to ContentPreset(kinds = listOf(LongFormContentEvent.KIND)), "Media" to ContentPreset(pseudoKind = "media"), "Channels" to ContentPreset(kinds = listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND)), "Communities" to ContentPreset(kinds = listOf(CommunityDefinitionEvent.KIND)), - "Wiki" to ContentPreset(kinds = listOf(WikiNoteEvent.KIND)), + "Wiki" to ContentPreset(kinds = listOf(WikiArticleEvent.KIND)), "Polls" to ContentPreset(kinds = listOf(PollEvent.KIND)), ) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/RenderableKinds.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/RenderableKinds.kt index b5be14e927..214e789056 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/RenderableKinds.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/RenderableKinds.kt @@ -33,12 +33,12 @@ import com.vitorpamplona.quartz.experimental.nns.NNSEvent import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent @@ -47,18 +47,18 @@ import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.videoCurationSet.VideoCurationSetEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent @@ -66,17 +66,17 @@ import com.vitorpamplona.quartz.nip5dNapplets.NamedNappletEvent import com.vitorpamplona.quartz.nip5dNapplets.RootNappletEvent import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nipA4PublicMessages.PublicMessageEvent import com.vitorpamplona.quartz.nipC0CodeSnippets.CodeSnippetEvent @@ -141,15 +141,15 @@ object RenderableKinds { setOf( MetadataEvent.KIND, ContactListEvent.KIND, - DeletionEvent.KIND, + DeletionRequestEvent.KIND, RepostEvent.KIND, ReactionEvent.KIND, GenericRepostEvent.KIND, RequestToVanishEvent.KIND, - FileHeaderEvent.KIND, + FileMetadataEvent.KIND, CommunityPostApprovalEvent.KIND, - LnZapRequestEvent.KIND, - LnZapEvent.KIND, + ZapRequestEvent.KIND, + ZapReceiptEvent.KIND, AppSpecificDataEvent.KIND, ) @@ -166,8 +166,8 @@ object RenderableKinds { TextNoteEvent.KIND, CommentEvent.KIND, PublicMessageEvent.KIND, - LongTextNoteEvent.KIND, - WikiNoteEvent.KIND, + LongFormContentEvent.KIND, + WikiArticleEvent.KIND, HighlightEvent.KIND, CodeSnippetEvent.KIND, NipTextEvent.KIND, @@ -175,8 +175,8 @@ object RenderableKinds { PictureEvent.KIND, VideoNormalEvent.KIND, VideoShortEvent.KIND, - VideoHorizontalEvent.KIND, - VideoVerticalEvent.KIND, + AddressableNormalVideoEvent.KIND, + AddressableShortVideoEvent.KIND, // A video list has a card of its own, so by the rule the omission list states — in // ALL when Amethyst can render it — it belongs in the window. Its title and // description are what Quartz indexes, which is exactly what a reader would search. @@ -202,15 +202,15 @@ object RenderableKinds { MeetingRoomEvent.KIND, // communities and lists CommunityDefinitionEvent.KIND, - PeopleListEvent.KIND, - FollowListEvent.KIND, + FollowSetEvent.KIND, + StarterPackEvent.KIND, BookmarkListEvent.KIND, OldBookmarkListEvent.KIND, PinListEvent.KIND, // calendar CalendarDateSlotEvent.KIND, CalendarTimeSlotEvent.KIND, - CalendarEvent.KIND, + CalendarCollectionEvent.KIND, // marketplace, apps, code and sites ClassifiedsEvent.KIND, SoftwareApplicationEvent.KIND, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultSorter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultSorter.kt index 0c1884976f..e1e4734441 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultSorter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultSorter.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.commons.util.KmpLock import com.vitorpamplona.amethyst.commons.util.sortedBySnapshot import com.vitorpamplona.amethyst.commons.util.withLock import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.utils.currentTimeSeconds object SearchResultSorter { @@ -66,7 +66,7 @@ object SearchResultSorter { } // Article title boost - if (event is LongTextNoteEvent) { + if (event is LongFormContentEvent) { val title = event.title()?.lowercase() if (title != null) { if (title.contains(query)) { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/nwc/NwcPaymentTracker.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/nwc/NwcPaymentTracker.kt index 94ba2218d7..697e71a1cd 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/nwc/NwcPaymentTracker.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/nwc/NwcPaymentTracker.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.commons.service.nwc import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent import com.vitorpamplona.quartz.utils.concurrent.ConcurrentMap import kotlin.concurrent.atomics.AtomicInt import kotlin.concurrent.atomics.ExperimentalAtomicApi @@ -60,7 +60,7 @@ class NwcPaymentTracker { class PendingRequest( val expectedServicePubkey: HexKey, val zappedNote: Note?, - val onResponse: suspend (LnZapPaymentResponseEvent) -> Unit, + val onResponse: suspend (NwcResponseEvent) -> Unit, ) { // Number of kind-23195 events that targeted this request id with the // right `e` tag but the wrong author. Surfaced to the UI when a @@ -74,7 +74,7 @@ class NwcPaymentTracker { /** * Registers a pending payment request. * - * @param requestId Event ID of the LnZapPaymentRequestEvent + * @param requestId Event ID of the NwcRequestEvent * @param expectedServicePubkey Wallet service pubkey from the request's `p` tag * @param zappedNote The note being zapped (null if not a zap payment) * @param onResponse Callback invoked when response arrives @@ -83,7 +83,7 @@ class NwcPaymentTracker { requestId: HexKey, expectedServicePubkey: HexKey, zappedNote: Note?, - onResponse: suspend (LnZapPaymentResponseEvent) -> Unit, + onResponse: suspend (NwcResponseEvent) -> Unit, ) { awaitingRequests[requestId] = PendingRequest(expectedServicePubkey, zappedNote, onResponse) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/pow/PoWPolicy.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/pow/PoWPolicy.kt index d4c0da8079..042127051c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/pow/PoWPolicy.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/pow/PoWPolicy.kt @@ -30,13 +30,13 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent @@ -116,7 +116,7 @@ object PoWPolicy { setOf( MetadataEvent.KIND, ContactListEvent.KIND, - LnZapRequestEvent.KIND, // blocks the invoice fetch + ZapRequestEvent.KIND, // blocks the invoice fetch OtsEvent.KIND, // machine-generated companion events DraftWrapEvent.KIND, // re-signed on a 1s debounce while typing ) @@ -141,7 +141,7 @@ object PoWPolicy { TextNoteEvent.KIND -> PoWCategory.SHORT_NOTES CommentEvent.KIND -> PoWCategory.COMMENTS ReportEvent.KIND -> PoWCategory.REPORTS - LongTextNoteEvent.KIND, HighlightEvent.KIND -> PoWCategory.LONG_FORM + LongFormContentEvent.KIND, HighlightEvent.KIND -> PoWCategory.LONG_FORM VoiceEvent.KIND, VoiceReplyEvent.KIND -> PoWCategory.VOICE RepostEvent.KIND, GenericRepostEvent.KIND -> PoWCategory.REPOSTS ReactionEvent.KIND -> PoWCategory.REACTIONS diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/LiveStreamTopZappersViewModel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/LiveStreamTopZappersViewModel.kt index 9c698f36d9..17e06618e1 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/LiveStreamTopZappersViewModel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/LiveStreamTopZappersViewModel.kt @@ -30,8 +30,8 @@ import com.vitorpamplona.amethyst.commons.nip53LiveActivities.LiveActivityTopZap import com.vitorpamplona.amethyst.commons.nip53LiveActivities.TopZapperEntry import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ZapContribution import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent import com.vitorpamplona.quartz.utils.toLongValue import kotlinx.coroutines.Dispatchers @@ -163,7 +163,7 @@ class LiveStreamTopZappersViewModel( private fun contributionFromStreamZap(note: Note): ZapContribution? = when (val ev = note.event) { - is LnZapEvent -> { + is ZapReceiptEvent -> { val request = ev.zapRequest ?: return null val sats = ev.amount()?.toLongValue() ?: return null ZapContribution(note.idHex, request.pubKey, request.isAnonTagged(), sats) @@ -179,12 +179,12 @@ class LiveStreamTopZappersViewModel( zapRequestNote: Note, receiptNote: Note?, ): ZapContribution? { - val receiptEv = receiptNote?.event as? LnZapEvent ?: return null - val request = zapRequestNote.event as? LnZapRequestEvent ?: return null + val receiptEv = receiptNote?.event as? ZapReceiptEvent ?: return null + val request = zapRequestNote.event as? ZapRequestEvent ?: return null val sats = receiptEv.amount()?.toLongValue() ?: return null return ZapContribution(receiptNote.idHex, request.pubKey, request.isAnonTagged(), sats) } } /** True for both public anonymous and NIP-57 private zaps (any `anon` tag, empty or encrypted). */ -private fun LnZapRequestEvent.isAnonTagged(): Boolean = tags.any { it.isNotEmpty() && it[0] == "anon" } +private fun ZapRequestEvent.isAnonTagged(): Boolean = tags.any { it.isNotEmpty() && it[0] == "anon" } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/RoomZapsState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/RoomZapsState.kt index ce0267fb9e..09af0fb088 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/RoomZapsState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/RoomZapsState.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.commons.viewmodels import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent import com.vitorpamplona.quartz.utils.toLongValue @@ -49,13 +49,13 @@ data class RoomZap( ) { companion object { /** - * Project a kind-9735 [LnZapEvent] into a [RoomZap]. The + * Project a kind-9735 [ZapReceiptEvent] into a [RoomZap]. The * source pubkey is the embedded kind-9734 request author — * the actual zapper — NOT the receipt's own `pubKey`, which * is the lnurl service provider's signing key. Falls back to * the receipt issuer only when the request can't be parsed. */ - fun from(event: LnZapEvent): RoomZap = + fun from(event: ZapReceiptEvent): RoomZap = RoomZap( eventId = event.id, sourcePubkey = event.zapRequest?.pubKey ?: event.pubKey, @@ -95,7 +95,7 @@ class RoomZapsAggregator { /** Apply one zap and return the post-evict snapshot. */ fun apply( - event: LnZapEvent, + event: ZapReceiptEvent, nowSec: Long, windowSec: Long, ): Map<String, List<RoomZap>> = apply(RoomZap.from(event), nowSec, windowSec) diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/actions/PrivateNoteFactoryTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/actions/PrivateNoteFactoryTest.kt index 29e8d2617e..b18e2da18e 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/actions/PrivateNoteFactoryTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/actions/PrivateNoteFactoryTest.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTags -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip17Dm.NIP17Factory import kotlinx.coroutines.test.runTest @@ -85,12 +85,12 @@ class PrivateNoteFactoryTest { val result = NIP17Factory().createDeletionNIP17( - template = DeletionEvent.build(listOf(reaction)), + template = DeletionRequestEvent.build(listOf(reaction)), to = listOf(bobSigner.pubKey), signer = aliceSigner, ) - assertEquals(DeletionEvent.KIND, result.msg.kind) + assertEquals(DeletionRequestEvent.KIND, result.msg.kind) assertEquals( setOf(aliceSigner.pubKey, bobSigner.pubKey), result.wraps.mapNotNull { it.recipientPubKey() }.toSet(), @@ -101,7 +101,7 @@ class PrivateNoteFactoryTest { val rumor = bobWrap.unwrapAndUnsealOrNull(bobSigner) assertNotNull(rumor, "recipient must be able to unwrap and unseal the deletion") - assertEquals(DeletionEvent.KIND, rumor.kind) + assertEquals(DeletionRequestEvent.KIND, rumor.kind) assertEquals(aliceSigner.pubKey, rumor.pubKey, "deletions only apply when the author matches") assertTrue(rumor.sig.isEmpty(), "the deletion travels as an unsigned rumor") assertTrue( diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/actions/ZapActionsTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/actions/ZapActionsTest.kt index eaa21809b1..e738de8961 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/actions/ZapActionsTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/actions/ZapActionsTest.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.utils.Secp256k1Instance import kotlinx.coroutines.test.runTest import kotlin.test.Test @@ -108,7 +108,7 @@ class ZapActionsTest { amountMillisats = 21_000L, inboxRelays = setOf(relay), comment = "thanks!", - zapType = LnZapEvent.ZapType.PUBLIC, + zapType = ZapReceiptEvent.ZapType.PUBLIC, lnurl = "lnurl1example", ) @@ -133,7 +133,7 @@ class ZapActionsTest { recipientPubkey = recipientPubkey, amountMillisats = 1_000L, inboxRelays = setOf(relay), - zapType = LnZapEvent.ZapType.ANONYMOUS, + zapType = ZapReceiptEvent.ZapType.ANONYMOUS, ) assertTrue( @@ -152,7 +152,7 @@ class ZapActionsTest { recipientPubkey = recipientPubkey, amountMillisats = 1_000L, inboxRelays = setOf(relay), - zapType = LnZapEvent.ZapType.PRIVATE, + zapType = ZapReceiptEvent.ZapType.PRIVATE, ) // NIP-57 PRIVATE zaps use an ephemeral key derived from diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadAssemblerTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadAssemblerTest.kt index 5bed98dc6f..04c4a0cacd 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadAssemblerTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/ThreadAssemblerTest.kt @@ -29,7 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.HintIndexer import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -75,7 +75,7 @@ class ThreadAssemblerTest { private val zap = Note(zapId).apply { - event = LnZapEvent(zapId, authorKey, 1001, arrayOf(arrayOf("e", originalId), arrayOf("p", authorKey)), "", sig) + event = ZapReceiptEvent(zapId, authorKey, 1001, arrayOf(arrayOf("e", originalId), arrayOf("p", authorKey)), "", sig) replyTo = listOf(original) } diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/ChatPreviewTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/ChatPreviewTest.kt index 35c6013608..d050ab8e93 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/ChatPreviewTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/ChatPreviewTest.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.model.privateChats -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.videoCurationSet.VideoCurationSetEvent @@ -40,7 +40,7 @@ class ChatPreviewTest { private fun privateDm( author: String = other, recipient: String = me, - ) = PrivateDmEvent( + ) = EncryptedDmEvent( id = "dm".padEnd(64, '0'), pubKey = author, createdAt = 1_000, diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/moderation/notifications/NotificationKindsTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/moderation/notifications/NotificationKindsTest.kt index 52ce93b5e1..f9aee77c88 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/moderation/notifications/NotificationKindsTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/moderation/notifications/NotificationKindsTest.kt @@ -21,14 +21,14 @@ package com.vitorpamplona.amethyst.commons.moderation.notifications import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent import kotlin.test.Test @@ -116,8 +116,8 @@ class NotificationKindsTest { ) } - private fun zapReceipt(pTag: String = me): LnZapEvent = - LnZapEvent( + private fun zapReceipt(pTag: String = me): ZapReceiptEvent = + ZapReceiptEvent( id = "zap".padEnd(64, '0'), pubKey = "lnurl_provider".padEnd(64, '0'), createdAt = 1_000, @@ -142,8 +142,8 @@ class NotificationKindsTest { private fun privateDm( author: String, pTag: String = me, - ): PrivateDmEvent = - PrivateDmEvent( + ): EncryptedDmEvent = + EncryptedDmEvent( id = "dm".padEnd(64, '0'), pubKey = author, createdAt = 1_000, @@ -303,7 +303,7 @@ class NotificationKindsTest { val expected = setOf( TextNoteEvent.KIND, - PrivateDmEvent.KIND, + EncryptedDmEvent.KIND, RepostEvent.KIND, ReactionEvent.KIND, GenericRepostEvent.KIND, @@ -311,7 +311,7 @@ class NotificationKindsTest { CommentEvent.KIND, GiftWrapEvent.KIND, NutzapEvent.KIND, - LnZapEvent.KIND, + ZapReceiptEvent.KIND, ) val actual = NotificationKinds.SUBSCRIPTION_KINDS.toSet() assertTrue(actual.containsAll(expected), "expected subset missing from SUBSCRIPTION_KINDS") diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletBrokerTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletBrokerTest.kt index a5d33e5a23..aa5d930cff 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletBrokerTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletBrokerTest.kt @@ -43,7 +43,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -137,7 +137,7 @@ class NappletBrokerTest { fromPublicKey: HexKey, ): String = throw NotImplementedError() - override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent = throw NotImplementedError() + override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = throw NotImplementedError() override suspend fun deriveKey(nonce: HexKey): HexKey = throw NotImplementedError() diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/onchain/OnchainZapSenderTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/onchain/OnchainZapSenderTest.kt index 8f439de480..d0f61a9ad3 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/onchain/OnchainZapSenderTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/onchain/OnchainZapSenderTest.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nipBCOnchainZaps.builder.OnchainZapBuilder import com.vitorpamplona.quartz.nipBCOnchainZaps.chain.BitcoinAddressTx import com.vitorpamplona.quartz.nipBCOnchainZaps.chain.BitcoinTx @@ -228,7 +228,7 @@ class OnchainZapSenderTest { fromPublicKey: HexKey, ) = inner.nip44Decrypt(ciphertext, fromPublicKey) - override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent = inner.decryptZapEvent(event) + override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = inner.decryptZapEvent(event) override suspend fun deriveKey(nonce: HexKey): HexKey = inner.deriveKey(nonce) diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/FilterRelayGroupStateTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/FilterRelayGroupStateTest.kt index d0ef97fdf9..5a410492c9 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/FilterRelayGroupStateTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/FilterRelayGroupStateTest.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupAdminsEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMembersEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupPinnedEvent -import com.vitorpamplona.quartz.nip29RelayGroups.metadata.SupportedRolesEvent +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupRolesEvent import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -51,7 +51,7 @@ class FilterRelayGroupStateTest { GroupMetadataEvent.KIND, GroupAdminsEvent.KIND, GroupMembersEvent.KIND, - SupportedRolesEvent.KIND, + GroupRolesEvent.KIND, ) private val pinKinds = listOf(GroupPinnedEvent.KIND) diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup/RelayGroupFilterBuildersTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup/RelayGroupFilterBuildersTest.kt index f7f85e742f..56c53c075d 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup/RelayGroupFilterBuildersTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup/RelayGroupFilterBuildersTest.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.quartz.buzz.forum.ForumCommentEvent import com.vitorpamplona.quartz.buzz.forum.ForumPostEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.FiltersChanged import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip29RelayGroups.GroupId @@ -32,8 +32,8 @@ import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupAdminsEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMembersEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupPinnedEvent -import com.vitorpamplona.quartz.nip29RelayGroups.metadata.SupportedRolesEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.DeleteEventEvent +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupRolesEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupDeleteEventEvent import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.GroupTag import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent @@ -187,7 +187,7 @@ class RelayGroupFilterBuildersTest { val filter = buildRelayGroupAuxFilter(GroupId("g1", relayA), 42L) assertEquals(relayA, filter.relay) - assertEquals(listOf(DeletionEvent.KIND, ReactionEvent.KIND, DeleteEventEvent.KIND), filter.filter.kinds) + assertEquals(listOf(DeletionRequestEvent.KIND, ReactionEvent.KIND, GroupDeleteEventEvent.KIND), filter.filter.kinds) assertEquals(listOf("g1"), filter.filter.tags!!["h"]) assertEquals(42L, filter.filter.since) assertEquals(RELAY_GROUP_AUX_LIMIT, filter.filter.limit) @@ -293,7 +293,7 @@ class RelayGroupFilterBuildersTest { @Test fun `directory kinds are 39000-39003 and exclude the pin list`() { assertEquals( - listOf(GroupMetadataEvent.KIND, GroupAdminsEvent.KIND, GroupMembersEvent.KIND, SupportedRolesEvent.KIND), + listOf(GroupMetadataEvent.KIND, GroupAdminsEvent.KIND, GroupMembersEvent.KIND, GroupRolesEvent.KIND), RELAY_GROUP_DIRECTORY_KINDS, ) assertFalse(RELAY_GROUP_DIRECTORY_KINDS.contains(GroupPinnedEvent.KIND), "the directory doesn't fetch each group's pins") diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/HomeMediaKindCoverageTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/HomeMediaKindCoverageTest.kt index c51e367053..813879a187 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/HomeMediaKindCoverageTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/HomeMediaKindCoverageTest.kt @@ -26,10 +26,10 @@ import com.vitorpamplona.amethyst.commons.relayClient.home.nip65Follows.HomePost import com.vitorpamplona.amethyst.commons.relayClient.home.nip72Communities.HomePostsFromCommunityKinds import com.vitorpamplona.amethyst.commons.relayClient.home.nip72Communities.HomePostsFromCommunityKindsStr import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -46,8 +46,8 @@ class HomeMediaKindCoverageTest { PictureEvent.KIND, VideoNormalEvent.KIND, VideoShortEvent.KIND, - VideoHorizontalEvent.KIND, - VideoVerticalEvent.KIND, + AddressableNormalVideoEvent.KIND, + AddressableShortVideoEvent.KIND, ) private fun assertCarriesMedia( diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip90AlgoFeeds/FilterHomePostsByAlgoFeedIdsTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip90AlgoFeeds/FilterHomePostsByAlgoFeedIdsTest.kt index fd94ee87ee..5376b735b3 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip90AlgoFeeds/FilterHomePostsByAlgoFeedIdsTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/home/nip90AlgoFeeds/FilterHomePostsByAlgoFeedIdsTest.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.commons.relayClient.home.nip90AlgoFeeds import com.vitorpamplona.amethyst.commons.model.topNavFeeds.favoriteAlgoFeeds.FavoriteAlgoFeedTopNavPerRelayFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.favoriteAlgoFeeds.FavoriteAlgoFeedTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.status.DvmStatusEvent import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -83,7 +83,7 @@ class FilterHomePostsByAlgoFeedIdsTest { val listen = filters.single() assertEquals(dvmRelay, listen.relay) assertEquals( - listOf(NIP90ContentDiscoveryResponseEvent.KIND, NIP90StatusEvent.KIND), + listOf(DvmContentDiscoveryResponseEvent.KIND, DvmStatusEvent.KIND), listen.filter.kinds, ) val eTag = listen.filter.tags?.get("e") diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultSorterTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultSorterTest.kt index 96217a9904..0afcf06b9e 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultSorterTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultSorterTest.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -51,14 +51,14 @@ class SearchResultSorterTest { createdAt: Long, content: String = "", title: String? = null, - ): LongTextNoteEvent { + ): LongFormContentEvent { val tags = if (title != null) { arrayOf(arrayOf("title", title)) } else { emptyArray() } - return LongTextNoteEvent( + return LongFormContentEvent( id = id, pubKey = "abc123def456abc123def456abc123def456abc123def456abc123def456abcd", createdAt = createdAt, diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/SearchSeedTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/SearchSeedTest.kt index 2d1ed9f50f..5cc5a7bf18 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/SearchSeedTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/SearchSeedTest.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.commons.search import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.quartz.nip01Core.core.Address -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import kotlinx.collections.immutable.persistentListOf import kotlin.test.Test @@ -46,7 +46,7 @@ class SearchSeedTest { @Test fun aKindWindowArrivesAsTheSameKinds() { - val seed = SearchSeed.ofKinds(LongTextNoteEvent.KIND) + val seed = SearchSeed.ofKinds(LongFormContentEvent.KIND) assertEquals("kind:article", QuerySerializer.serialize(seed)) assertEquals(seed, throughTheField(seed)) } @@ -196,7 +196,7 @@ class SearchSeedTest { assertTrue(SearchQuery(scopes = persistentListOf(ExternalScope("isbn", "123"))).pinsToNotes) assertTrue(SearchQuery(groups = persistentListOf("abc")).pinsToNotes) // Every seeded feed window pins it too. - assertTrue(SearchSeed.ofKinds(LongTextNoteEvent.KIND).pinsToNotes) + assertTrue(SearchSeed.ofKinds(LongFormContentEvent.KIND).pinsToNotes) } @Test diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/RoomZapsStateTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/RoomZapsStateTest.kt index bc67bc0981..6ac9dc3d7d 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/RoomZapsStateTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/RoomZapsStateTest.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.viewmodels -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull @@ -46,13 +46,13 @@ class RoomZapsStateTest { to: String?, createdAt: Long, id: String = (nextEventId++).toString(16).padStart(64, '0'), - ): LnZapEvent { + ): ZapReceiptEvent { val tags = buildList<Array<String>> { add(arrayOf("a", "30312:host:room")) if (to != null) add(arrayOf("p", to)) }.toTypedArray() - return LnZapEvent( + return ZapReceiptEvent( id = id, pubKey = from, createdAt = createdAt, diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/ops/CashuWalletOps.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/ops/CashuWalletOps.kt index 99def91987..e26b1471d1 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/ops/CashuWalletOps.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/ops/CashuWalletOps.kt @@ -29,7 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.aTag.aTag -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip60Cashu.bdhke.Bdhke import com.vitorpamplona.quartz.nip60Cashu.history.CashuSpendingHistoryEvent import com.vitorpamplona.quartz.nip60Cashu.history.SpendingDirection @@ -195,7 +195,7 @@ class CashuWalletOps( * bails when it has no shared mint or no P2PK pubkey. This is the * durable signal. * 2. NIP-09 delete the now-empty kind:10019 by its address coordinate - * (DeletionEvent.build adds the `a` tag automatically because the + * (DeletionRequestEvent.build adds the `a` tag automatically because the * event is replaceable) so relays that DO honor deletions drop it * entirely. Deletions are optional on Nostr, which is why step 1 * runs first as the fallback. @@ -209,7 +209,7 @@ class CashuWalletOps( val emptyEvent = signer.sign(emptyTemplate) publish(emptyEvent) - val delTemplate = DeletionEvent.build(listOf(emptyEvent)) + val delTemplate = DeletionRequestEvent.build(listOf(emptyEvent)) val delEvent = signer.sign(delTemplate) publish(delEvent) } @@ -230,7 +230,7 @@ class CashuWalletOps( suspend fun deleteWallet(walletEvent: CashuWalletEvent) { stopNutzaps() - val delTemplate = DeletionEvent.build(listOf(walletEvent)) + val delTemplate = DeletionRequestEvent.build(listOf(walletEvent)) val delEvent = signer.sign(delTemplate) publish(delEvent) } @@ -286,7 +286,7 @@ class CashuWalletOps( * fired anyway; running it twice on a missing event is harmless. */ suspend fun cancelMintQuote(quoteEvent: CashuMintQuoteEvent) { - val delTemplate = DeletionEvent.build(listOf(quoteEvent)) + val delTemplate = DeletionRequestEvent.build(listOf(quoteEvent)) val delEvent = signer.sign(delTemplate) publish(delEvent) } @@ -363,7 +363,7 @@ class CashuWalletOps( publish(historyEvent) // NIP-09 delete the now-fulfilled quote event. - val delTemplate = DeletionEvent.build(listOf(quoteEvent)) + val delTemplate = DeletionRequestEvent.build(listOf(quoteEvent)) val delEvent = signer.sign(delTemplate) publish(delEvent) @@ -527,11 +527,11 @@ class CashuWalletOps( } // NIP-09 delete the source token events. Per spec, also need k=7375 tag, - // which DeletionEvent.build adds automatically from the source event kind. + // which DeletionRequestEvent.build adds automatically from the source event kind. val deleteEvent = run { val toDelete = selected.map { it.event } + listOfNotNull(prePaidChangeEvent.takeIf { finalChangeEvent != null }) - val delTemplate = DeletionEvent.build(toDelete) + val delTemplate = DeletionRequestEvent.build(toDelete) signer.sign(delTemplate).also { publish(it) } } @@ -598,7 +598,7 @@ class CashuWalletOps( val deleteEvent = run { - val template = DeletionEvent.build(selected.map { it.event }) + val template = DeletionRequestEvent.build(selected.map { it.event }) signer.sign(template).also { publish(it) } } @@ -769,7 +769,7 @@ class CashuWalletOps( // NIP-09 delete the source token events. val deleteEvent = run { - val template = DeletionEvent.build(selected.map { it.event }) + val template = DeletionRequestEvent.build(selected.map { it.event }) signer.sign(template).also { publish(it) } @@ -1001,7 +1001,7 @@ class CashuWalletOps( entry.content.proofs.any { states[it.secret] == ProofState.SPENT } } if (stale.isEmpty()) return emptyList() - val delTemplate = DeletionEvent.build(stale.map { it.event }) + val delTemplate = DeletionRequestEvent.build(stale.map { it.event }) publish(signer.sign(delTemplate)) return stale.map { it.event } } @@ -1046,7 +1046,7 @@ class CashuWalletOps( val signed = signer.sign(template) publish(signed) - val delTemplate = DeletionEvent.build(entries.map { it.event }) + val delTemplate = DeletionRequestEvent.build(entries.map { it.event }) val delEvent = signer.sign(delTemplate) publish(delEvent) @@ -1088,7 +1088,7 @@ class CashuWalletOps( * kind:38000 is parameterized-replaceable — a delete with an `a` tag * pointing at the address coordinate (kind:pubkey:dTag) drops all * versions on compliant relays. We also include the original event id - * via DeletionEvent.build so relays that only track by event id still + * via DeletionRequestEvent.build so relays that only track by event id still * remove it. MintRecommendationEvent doesn't extend AddressableEvent * today, so we compute and add the `a` tag ourselves. */ @@ -1099,7 +1099,7 @@ class CashuWalletOps( // d-tag still get a NIP-09 `e`-only delete (the default build path). val dTag = event.dTag() val template = - DeletionEvent.build(listOf(event)) { + DeletionRequestEvent.build(listOf(event)) { if (dTag != null) aTag(Address(event.kind, event.pubKey, dTag)) } val delEvent = signer.sign(template) @@ -1497,14 +1497,14 @@ data class MeltCompleted( val paidAmount: Long, val fees: Long, val historyEvent: CashuSpendingHistoryEvent, - val deleteEvent: DeletionEvent, + val deleteEvent: DeletionRequestEvent, val newTokenEvent: CashuTokenEvent?, ) data class SendTokenCompleted( val cashuToken: String, val amount: Long, - val deleteEvent: DeletionEvent, + val deleteEvent: DeletionRequestEvent, val keepEvent: CashuTokenEvent?, val historyEvent: CashuSpendingHistoryEvent, ) @@ -1519,7 +1519,7 @@ data class RedeemCompleted( data class NutzapSent( val nutzapEvent: NutzapEvent, val keepEvent: CashuTokenEvent?, - val deleteEvent: DeletionEvent, + val deleteEvent: DeletionRequestEvent, val historyEvent: CashuSpendingHistoryEvent, val amount: Long, ) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nip52Calendar/IcsExport.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nip52Calendar/IcsExport.kt index ecf32fda47..2c5e7b579b 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nip52Calendar/IcsExport.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nip52Calendar/IcsExport.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.commons.model.nip52Calendar import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import java.time.Instant import java.time.LocalDate import java.time.ZoneOffset @@ -71,7 +71,7 @@ object IcsExport { * haven't been fetched from relays yet. */ fun calendarToIcs( - calendar: CalendarEvent, + calendar: CalendarCollectionEvent, memberEvents: List<Pair<Address, Any>>, nowSeconds: Long, ): String { @@ -110,7 +110,7 @@ object IcsExport { return safeFilename(title ?: address.dTag) + ".ics" } - fun calendarFilename(calendar: CalendarEvent): String = safeFilename(calendar.title() ?: "calendar") + ".ics" + fun calendarFilename(calendar: CalendarCollectionEvent): String = safeFilename(calendar.title() ?: "calendar") + ".ics" private fun safeFilename(raw: String): String = raw diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nip57Zaps/ZapAction.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nip57Zaps/ZapAction.kt index 34e116b43b..0066a7ed89 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nip57Zaps/ZapAction.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/nip57Zaps/ZapAction.kt @@ -24,8 +24,8 @@ import com.vitorpamplona.amethyst.commons.service.lnurl.LightningAddressResolver import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent /** * Handles NIP-57 zap requests and invoice fetching. @@ -66,7 +66,7 @@ object ZapAction { relays: Set<NormalizedRelayUrl>, signer: NostrSigner, resolver: LightningAddressResolver, - zapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PUBLIC, + zapType: ZapReceiptEvent.ZapType = ZapReceiptEvent.ZapType.PUBLIC, onProgress: (Float) -> Unit = {}, ): ZapResult { if (!signer.isWriteable()) { @@ -76,7 +76,7 @@ object ZapAction { // Create zap request using quartz factory val zapRequest = try { - LnZapRequestEvent.create( + ZapRequestEvent.create( zappedEvent = targetEvent, relays = relays, signer = signer, @@ -120,7 +120,7 @@ object ZapAction { relays: Set<NormalizedRelayUrl>, signer: NostrSigner, resolver: LightningAddressResolver, - zapType: LnZapEvent.ZapType = LnZapEvent.ZapType.PUBLIC, + zapType: ZapReceiptEvent.ZapType = ZapReceiptEvent.ZapType.PUBLIC, onProgress: (Float) -> Unit = {}, ): ZapResult { if (!signer.isWriteable()) { @@ -130,7 +130,7 @@ object ZapAction { // Create zap request for user val zapRequest = try { - LnZapRequestEvent.create( + ZapRequestEvent.create( userHex = userPubHex, relays = relays, signer = signer, diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/lnurl/LightningAddressResolver.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/lnurl/LightningAddressResolver.kt index 5acca96399..a675147433 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/lnurl/LightningAddressResolver.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/lnurl/LightningAddressResolver.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.commons.service.lnurl import com.fasterxml.jackson.databind.ObjectMapper import com.vitorpamplona.quartz.lightning.LnInvoiceUtil import com.vitorpamplona.quartz.lightning.Lud06 -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointCache import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointInfo import kotlinx.coroutines.Dispatchers @@ -94,7 +94,7 @@ class LightningAddressResolver( lnAddress: String, milliSats: Long, message: String = "", - zapRequest: LnZapRequestEvent? = null, + zapRequest: ZapRequestEvent? = null, onProgress: (Float) -> Unit = {}, ): Result = withContext(Dispatchers.IO) { @@ -209,7 +209,7 @@ class LightningAddressResolver( callbackUrl: String, milliSats: Long, message: String, - zapRequest: LnZapRequestEvent?, + zapRequest: ZapRequestEvent?, ): String? = withContext(Dispatchers.IO) { try { diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/lnurl/OkHttpLnurlEndpointResolver.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/lnurl/OkHttpLnurlEndpointResolver.kt index dd162f881c..2755642c0f 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/lnurl/OkHttpLnurlEndpointResolver.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/lnurl/OkHttpLnurlEndpointResolver.kt @@ -32,7 +32,7 @@ import okhttp3.coroutines.executeAsync import kotlin.coroutines.cancellation.CancellationException /** - * OkHttp-backed [LnurlEndpointResolver]. Used by `LocalCache.consume(LnZapEvent)` + * OkHttp-backed [LnurlEndpointResolver]. Used by `LocalCache.consume(ZapReceiptEvent)` * to look up a recipient's LNURL provider's `nostrPubkey` when validating an * incoming zap receipt (NIP-57 Appendix F). * diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModel.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModel.kt index 80ad79160f..faf5d970f1 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModel.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModel.kt @@ -629,7 +629,7 @@ class NestViewModel( * same window so the visual cadence matches the React button. */ fun onZapEvent( - event: com.vitorpamplona.quartz.nip57Zaps.LnZapEvent, + event: com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent, nowSec: Long, windowSec: Long = REACTION_WINDOW_SEC, ) { diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotEditsAndSystemRowsTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotEditsAndSystemRowsTest.kt index 6e27962957..e2ea1de62e 100644 --- a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotEditsAndSystemRowsTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotEditsAndSystemRowsTest.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.marmot.mip01Groups.MarmotGroupData import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import kotlinx.coroutines.runBlocking import kotlin.test.Test import kotlin.test.assertEquals @@ -130,7 +130,7 @@ class MarmotEditsAndSystemRowsTest { val forged = MarmotAppEvent.build( pubKey = impostor, - kind = DeletionEvent.KIND, + kind = DeletionRequestEvent.KIND, content = "", createdAt = 1_800_000_000L, tags = arrayOf(arrayOf("e", mine.innerEvent.id)), @@ -168,7 +168,7 @@ class MarmotEditsAndSystemRowsTest { val forged = MarmotAppEvent.build( pubKey = f.signer.pubKey, - kind = DeletionEvent.KIND, + kind = DeletionRequestEvent.KIND, content = "", createdAt = 1_800_000_000L, tags = arrayOf(arrayOf("e", absent)), diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip52Calendar/IcsExportTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip52Calendar/IcsExportTest.kt index f6e5ec7e81..11cdde002e 100644 --- a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip52Calendar/IcsExportTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip52Calendar/IcsExportTest.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.commons.model.nip52Calendar import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -181,7 +181,7 @@ class IcsExportTest { sig = "sig", ) val calendar = - CalendarEvent( + CalendarCollectionEvent( id = "cal", pubKey = "pub", createdAt = 0L, diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip71Video/DivineVideoInteropTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip71Video/DivineVideoInteropTest.kt index c476dab342..db2424d860 100644 --- a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip71Video/DivineVideoInteropTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip71Video/DivineVideoInteropTest.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip51Lists.videoCurationSet.VideoCurationSetEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.credits.CreditTarget import com.vitorpamplona.quartz.nip71Video.credits.VideoCredit import com.vitorpamplona.quartz.nip71Video.textTrack.TextTrackEvent @@ -86,20 +86,20 @@ class DivineVideoInteropTest { fun aShortVideoIsAnAddressableVerticalVideo() { val event = Event.fromJson(video) - assertTrue(event is VideoVerticalEvent) + assertTrue(event is AddressableShortVideoEvent) // The address must come from the `d` tag: an `a` tag elsewhere on the network points at // `34236:<pubkey>:<d>`, and a class on the wrong base would split the cache in two. assertEquals( "34236:4d7dccc0a5116daa057348ef79c573873cddd9eff066fc6a5f3d37e8264afbeb:c855df3d07ba963e9097d5a151b0c14a0a3d494e4ff9b04a389bc0b5f5c16862", (event as AddressableEvent).address().toValue(), ) - assertEquals("Xmas Already??", (event as VideoVerticalEvent).title()) + assertEquals("Xmas Already??", (event as AddressableShortVideoEvent).title()) assertEquals(6, event.duration()) } @Test fun theBlossomUrlIsPlayedAsAVideoDespiteHavingNoFileExtension() { - val track = (Event.fromJson(video) as VideoVerticalEvent).selectVideoTrack() + val track = (Event.fromJson(video) as AddressableShortVideoEvent).selectVideoTrack() assertNotNull(track) assertEquals("https://media.divine.video/c855df3d07ba963e9097d5a151b0c14a0a3d494e4ff9b04a389bc0b5f5c16862", track!!.url) @@ -125,7 +125,7 @@ class DivineVideoInteropTest { // itself. LocalCache.computeReplyTo has no branch for video events, so this never turns // the post into a reply — which would drop it out of the home feed and thread it under // whatever video the audio came from. - val event = Event.fromJson(video) as VideoVerticalEvent + val event = Event.fromJson(video) as AddressableShortVideoEvent val audio = event.tags.first { it[0] == "e" } assertEquals("218af5fc90d66a16ce273f00a4e412a71443441c04c67d1e34ff99c654871d3d", audio[1]) @@ -136,7 +136,7 @@ class DivineVideoInteropTest { fun captionsComeFromTheTextTrackTagsAndNotTheAudioPointer() { // textTrack() used to run ETag::parse, so it returned the `e` tags instead — on this // event, the reused-soundtrack pointer, which is not a caption track at all. - val tracks = (Event.fromJson(video) as VideoVerticalEvent).textTrack() + val tracks = (Event.fromJson(video) as AddressableShortVideoEvent).textTrack() assertEquals(2, tracks.size) // Divine publishes the same track twice: the WebVTT file on Blossom, and the addressable @@ -153,7 +153,7 @@ class DivineVideoInteropTest { @Test fun theTwoFormsOfTheSameCaptionTrackCollapseIntoOne() { - val video = Event.fromJson(video) as VideoVerticalEvent + val video = Event.fromJson(video) as AddressableShortVideoEvent val refs = video.captionTracks() // One `text-track` is directly loadable, the other has to be fetched first. @@ -230,7 +230,7 @@ class DivineVideoInteropTest { @Test fun creditsReadTheMarkerWhicheverSlotItIsIn() { // divine-mobile: ["p", <pubkey>, <relay>, "inspired-by"] and ["e", <id>, <relay>, "audio"]. - val credits = (Event.fromJson(videoWithCredits) as VideoVerticalEvent).credits() + val credits = (Event.fromJson(videoWithCredits) as AddressableShortVideoEvent).credits() val person = credits.filterIsInstance<VideoCredit>().first { it.target is CreditTarget.Person } assertEquals("0db19e8f1b13e03cd07379edd37a71f07b08a4fb5e7eac222ba1d2c823807075", (person.target as CreditTarget.Person).pubKey) @@ -250,7 +250,7 @@ class DivineVideoInteropTest { // divine-web's collaborator invite writes ["p", <pubkey>, "<role>"] — no relay hint at all, // so the slot PTag reads for a hint holds the label instead. val invite = - VideoVerticalEvent( + AddressableShortVideoEvent( id = "a".repeat(64), pubKey = "b".repeat(64), createdAt = 1789666628, @@ -269,7 +269,7 @@ class DivineVideoInteropTest { // No marker means nothing is being credited — printing "references <id>" for a stray // threading tag would invent an attribution the publisher never made. val stray = - VideoVerticalEvent( + AddressableShortVideoEvent( id = "a".repeat(64), pubKey = "b".repeat(64), createdAt = 1789666628, diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip71Video/VideoTrackSelectionTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip71Video/VideoTrackSelectionTest.kt index 0a7453ddc8..e92f88c732 100644 --- a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip71Video/VideoTrackSelectionTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip71Video/VideoTrackSelectionTest.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.commons.model.nip71Video +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoMeta -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import org.junit.Assert.assertEquals import org.junit.Assert.assertNull @@ -36,7 +36,7 @@ class VideoTrackSelectionTest { private val hls = "application/vnd.apple.mpegurl" private fun event(vararg metas: VideoMeta) = - VideoVerticalEvent( + AddressableShortVideoEvent( id = "bfe2f2244fefc7cebc7b2eae825495f99dabb4649ee3f90ab1fa33bcd1e9bb9f", pubKey = "3b6187c08b9dd5617150ea047e788a0fdd44b4394cb5566cba76f683ddc027d2", createdAt = 1780894816, diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/nip17Dm/DmInboxRelayResolverOutboxTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/nip17Dm/DmInboxRelayResolverOutboxTest.kt index 417ef9478d..d539f693fc 100644 --- a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/nip17Dm/DmInboxRelayResolverOutboxTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/relayClient/nip17Dm/DmInboxRelayResolverOutboxTest.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals @@ -60,11 +60,11 @@ class DmInboxRelayResolverOutboxTest { ) private fun dm10050(inbox: List<NormalizedRelayUrl>) = - ChatMessageRelayListEvent( + DmRelayListEvent( id = "c".repeat(64), pubKey = peer, createdAt = 1_700_000_050, - tags = ChatMessageRelayListEvent.createTagArray(inbox), + tags = DmRelayListEvent.createTagArray(inbox), content = "", sig = dummySig(), ) @@ -115,7 +115,7 @@ class DmInboxRelayResolverOutboxTest { // Indexer knows WHERE the peer writes (10002) but not their 10050. client.scriptEvent(AdvertisedRelayListEvent.KIND, indexer, listOf(outbox10002(listOf(peerOutbox)))) // The peer's own write relay serves their 10050. - client.scriptEvent(ChatMessageRelayListEvent.KIND, peerOutbox, listOf(dm10050(listOf(peerDmInbox)))) + client.scriptEvent(DmRelayListEvent.KIND, peerOutbox, listOf(dm10050(listOf(peerDmInbox)))) val resolver = DmInboxRelayResolver( @@ -136,7 +136,7 @@ class DmInboxRelayResolverOutboxTest { runBlocking { val client = ScriptedClient() // No 10002 on the indexer; the write relay is only known from cache. - client.scriptEvent(ChatMessageRelayListEvent.KIND, peerOutbox, listOf(dm10050(listOf(peerDmInbox)))) + client.scriptEvent(DmRelayListEvent.KIND, peerOutbox, listOf(dm10050(listOf(peerDmInbox)))) val resolver = DmInboxRelayResolver( diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModelTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModelTest.kt index 10293574a4..5c27c8c828 100644 --- a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModelTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModelTest.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -589,7 +589,7 @@ class NestViewModelTest { fromPublicKey: String, ): String = error("not used") - override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent = error("not used") + override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = error("not used") override suspend fun deriveKey(nonce: String): String = error("not used") diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip17Dm/ui/IncognitoBadge.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip17Dm/ui/IncognitoBadge.kt index 2c7269e112..d0af55cff6 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip17Dm/ui/IncognitoBadge.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip17Dm/ui/IncognitoBadge.kt @@ -29,11 +29,11 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.ui.theme.IncognitoIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent @Composable fun IncognitoBadge(baseNote: Note) { - if (baseNote.event is PrivateDmEvent) { + if (baseNote.event is EncryptedDmEvent) { Icon( symbol = MaterialSymbols.NoEncryption, contentDescription = null, diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/CalendarCollectionCard.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/CalendarCollectionCard.kt index ba22fa6639..6066ad2cab 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/CalendarCollectionCard.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/CalendarCollectionCard.kt @@ -41,12 +41,12 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.calendar_collection_count import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import org.jetbrains.compose.resources.pluralStringResource /** Card for a NIP-52 calendar collection (kind 31924): title, description, and event count. */ @Composable -fun CalendarCollectionCard(event: CalendarEvent) { +fun CalendarCollectionCard(event: CalendarCollectionEvent) { val title = remember(event) { event.title() } val eventCount = remember(event) { event.calendarEventAddresses().size } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index c3aa87d9c9..a25cac2394 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -167,7 +167,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent @@ -1817,7 +1817,7 @@ fun MainContent( kinds = listOf( AdvertisedRelayListEvent.KIND, - ChatMessageRelayListEvent.KIND, + DmRelayListEvent.KIND, SearchRelayListEvent.KIND, BlockedRelayListEvent.KIND, BlossomServersEvent.KIND, @@ -1845,7 +1845,7 @@ fun MainContent( // dmInboxRelaysStrict, self-copy resolution) alongside // accountRelays' persisted copy. if (event is AdvertisedRelayListEvent || - event is ChatMessageRelayListEvent || + event is DmRelayListEvent || event is BlossomServersEvent || event is MuteListEvent ) { @@ -1877,7 +1877,7 @@ fun MainContent( kinds = listOf( AdvertisedRelayListEvent.KIND, - ChatMessageRelayListEvent.KIND, + DmRelayListEvent.KIND, SearchRelayListEvent.KIND, BlockedRelayListEvent.KIND, BlossomServersEvent.KIND, @@ -1926,7 +1926,7 @@ fun MainContent( // Process into chatroomList based on event type when (event) { - is com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent -> { + is com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent -> { iAccount.chatroomList.addMessage( event.chatroomKey(iAccount.pubKey), note, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt index 1d201d6c6c..39db2557dd 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt @@ -44,7 +44,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip18Reposts.quotes.QAddressableTag @@ -52,17 +52,17 @@ import com.vitorpamplona.quartz.nip18Reposts.quotes.QEventTag import com.vitorpamplona.quartz.nip18Reposts.quotes.taggedQuotes import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent @@ -310,11 +310,11 @@ class DesktopLocalCache : ICacheProvider { consumeReaction(event, relay) } - is LnZapRequestEvent -> { + is ZapRequestEvent -> { consumeZapRequest(event, relay) } - is LnZapEvent -> { + is ZapReceiptEvent -> { consumeZap(event, relay) } @@ -330,7 +330,7 @@ class DesktopLocalCache : ICacheProvider { consumeContactList(event) } - is LongTextNoteEvent -> { + is LongFormContentEvent -> { consumeLongTextNote(event, relay) } @@ -342,7 +342,7 @@ class DesktopLocalCache : ICacheProvider { consumeOldBookmarkList(event) } - is FollowListEvent -> { + is StarterPackEvent -> { consumeFollowList(event) } @@ -354,7 +354,7 @@ class DesktopLocalCache : ICacheProvider { consumeAdvertisedRelayList(event, relay) } - is ChatMessageRelayListEvent -> { + is DmRelayListEvent -> { consumeChatMessageRelayList(event, relay) } @@ -436,7 +436,7 @@ class DesktopLocalCache : ICacheProvider { * the event stream because the UI doesn't render kind 10050s directly. */ private fun consumeChatMessageRelayList( - event: ChatMessageRelayListEvent, + event: DmRelayListEvent, relay: NormalizedRelayUrl?, ): Boolean { val addressableNote = getOrCreateAddressableNote(event.address()) @@ -586,10 +586,10 @@ class DesktopLocalCache : ICacheProvider { /** * Consumes a kind 9734 zap request event. - * Must be consumed before the corresponding LnZapEvent (kind 9735). + * Must be consumed before the corresponding ZapReceiptEvent (kind 9735). */ private fun consumeZapRequest( - event: LnZapRequestEvent, + event: ZapRequestEvent, relay: NormalizedRelayUrl?, ): Boolean { val note = getOrCreateNote(event.id) @@ -605,7 +605,7 @@ class DesktopLocalCache : ICacheProvider { * Links zap to target notes via the embedded zap request. */ private fun consumeZap( - event: LnZapEvent, + event: ZapReceiptEvent, relay: NormalizedRelayUrl?, ): Boolean { val note = getOrCreateNote(event.id) @@ -739,7 +739,7 @@ class DesktopLocalCache : ICacheProvider { * Creates Note in cache like TextNoteEvent. */ private fun consumeLongTextNote( - event: LongTextNoteEvent, + event: LongFormContentEvent, relay: NormalizedRelayUrl?, ): Boolean { val note = getOrCreateNote(event.id) @@ -788,7 +788,7 @@ class DesktopLocalCache : ICacheProvider { private val _followPackVersion = MutableStateFlow(0L) val followPackVersion: StateFlow<Long> = _followPackVersion.asStateFlow() - private fun consumeFollowList(event: FollowListEvent): Boolean { + private fun consumeFollowList(event: StarterPackEvent): Boolean { val address = event.address() val addressableNote = getOrCreateAddressableNote(address) val author = getOrCreateUser(event.pubKey) @@ -883,14 +883,14 @@ class DesktopLocalCache : ICacheProvider { } /** - * Snapshot all 39089 (FollowListEvent) addressable notes currently in cache. + * Snapshot all 39089 (StarterPackEvent) addressable notes currently in cache. * Use [followPackVersion] to drive reactive recomputation. */ - fun snapshotFollowPacks(): List<FollowListEvent> { - val out = mutableListOf<FollowListEvent>() + fun snapshotFollowPacks(): List<StarterPackEvent> { + val out = mutableListOf<StarterPackEvent>() addressableNotes.forEach { _, note -> val ev = note.event - if (ev is FollowListEvent) out.add(ev) + if (ev is StarterPackEvent) out.add(ev) } return out } @@ -908,10 +908,10 @@ class DesktopLocalCache : ICacheProvider { * @return true if event was processed, false if already seen */ fun consume( - event: LnZapPaymentRequestEvent, + event: NwcRequestEvent, zappedNote: Note?, relay: NormalizedRelayUrl?, - onResponse: suspend (LnZapPaymentResponseEvent) -> Unit, + onResponse: suspend (NwcResponseEvent) -> Unit, wasVerified: Boolean = false, ): Boolean { if (!wasVerified && !justVerify(event)) return false @@ -945,7 +945,7 @@ class DesktopLocalCache : ICacheProvider { */ @OptIn(DelicateCoroutinesApi::class) fun consume( - event: LnZapPaymentResponseEvent, + event: NwcResponseEvent, relay: NormalizedRelayUrl?, wasVerified: Boolean = false, ): Boolean { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/feeds/DesktopFeedFilters.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/feeds/DesktopFeedFilters.kt index 2dba33235d..9e0830cebe 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/feeds/DesktopFeedFilters.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/feeds/DesktopFeedFilters.kt @@ -35,9 +35,9 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent private fun isFeedNote(event: Event?): Boolean = @@ -317,11 +317,11 @@ class DesktopReadsFeedFilter( override fun feed(): List<Note> = cache.notes - .filterIntoSet { _, note -> note.event is LongTextNoteEvent && !note.isHiddenFor(hidden()) } + .filterIntoSet { _, note -> note.event is LongFormContentEvent && !note.isHiddenFor(hidden()) } .sortedWith(DefaultFeedOrder) .take(limit()) - override fun applyFilter(newItems: Set<Note>): Set<Note> = newItems.filterTo(HashSet()) { it.event is LongTextNoteEvent && !it.isHiddenFor(hidden()) } + override fun applyFilter(newItems: Set<Note>): Set<Note> = newItems.filterTo(HashSet()) { it.event is LongFormContentEvent && !it.isHiddenFor(hidden()) } override fun sort(items: Set<Note>): List<Note> = items.sortedWith(DefaultFeedOrder) @@ -342,7 +342,7 @@ class DesktopNotificationFeedFilter( setOf( TextNoteEvent.KIND, ReactionEvent.KIND, - LnZapEvent.KIND, + ZapReceiptEvent.KIND, ) } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/BulkFollowAction.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/BulkFollowAction.kt index bcae97ce02..440f4aee46 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/BulkFollowAction.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/BulkFollowAction.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache import com.vitorpamplona.amethyst.desktop.model.DesktopIAccount import com.vitorpamplona.amethyst.desktop.network.RelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent data class BulkFollowPreview( val totalMembers: Int, @@ -52,7 +52,7 @@ object BulkFollowAction { * cached metadata (so the bulk-follow always carries relay hints). */ fun computePreview( - pack: FollowListEvent, + pack: StarterPackEvent, cache: DesktopLocalCache, currentFollows: Set<HexKey>, myPubkey: HexKey, @@ -101,7 +101,7 @@ object BulkFollowAction { /** Unfollows every pack member that the user currently follows. */ suspend fun commitUnfollowAll( - pack: FollowListEvent, + pack: StarterPackEvent, iAccount: DesktopIAccount, relayManager: RelayConnectionManager, cache: DesktopLocalCache, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/FollowPackEditor.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/FollowPackEditor.kt index 0e11b07b30..d08cf586f1 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/FollowPackEditor.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/FollowPackEditor.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.desktop.followpacks import com.vitorpamplona.quartz.nip01Core.core.Address -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent /** * Lightweight helpers for follow-pack viewing actions. @@ -30,12 +30,12 @@ import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent */ object FollowPackEditor { /** Encodes the pack as a `nostr:naddr1…` URI for sharing via clipboard. */ - fun toShareUri(pack: FollowListEvent): String { + fun toShareUri(pack: StarterPackEvent): String { val naddr = com.vitorpamplona.quartz.nip19Bech32.entities .NAddress .create( - kind = FollowListEvent.KIND, + kind = StarterPackEvent.KIND, pubKeyHex = pack.pubKey, dTag = pack.dTag(), relays = emptyList(), @@ -44,5 +44,5 @@ object FollowPackEditor { } /** Address coordinate (e.g. "39089:<pubkey>:<dTag>") — stable identity. */ - fun aTag(pack: FollowListEvent): String = Address.assemble(FollowListEvent.KIND, pack.pubKey, pack.dTag()) + fun aTag(pack: StarterPackEvent): String = Address.assemble(StarterPackEvent.KIND, pack.pubKey, pack.dTag()) } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/FollowPacksState.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/FollowPacksState.kt index cb506f3282..baac2023fc 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/FollowPacksState.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/FollowPacksState.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -81,7 +81,7 @@ class FollowPacksState( } /** All 39089 events currently in the cache. Recomputes on cache change. */ - val allPacks: StateFlow<List<FollowListEvent>> = + val allPacks: StateFlow<List<StarterPackEvent>> = cache.followPackVersion .map { cache.snapshotFollowPacks().sortedByDescending { it.createdAt } } .stateIn( @@ -91,7 +91,7 @@ class FollowPacksState( ) /** Packs authored by accounts the user follows. Top-3 for sidebar. */ - val followedAuthorsPacks: StateFlow<List<FollowListEvent>> = + val followedAuthorsPacks: StateFlow<List<StarterPackEvent>> = combine(allPacks, followedAuthors) { packs, followed -> packs.filter { it.pubKey in followed } }.stateIn( @@ -104,7 +104,7 @@ class FollowPacksState( * Sidebar top-3: own packs + packs by people user follows. * Falls back to most-recent-from-relays when user has no follow-derived packs. */ - val sidebarTopPacks: StateFlow<List<FollowListEvent>> = + val sidebarTopPacks: StateFlow<List<StarterPackEvent>> = followedAuthorsPacks .map { followedPacks -> if (followedPacks.isNotEmpty()) { @@ -128,8 +128,8 @@ class FollowPacksState( private val recentlyShown = ArrayDeque<String>(5) - private val featuredPackInternal = MutableStateFlow<FollowListEvent?>(null) - val featuredPack: StateFlow<FollowListEvent?> = featuredPackInternal.asStateFlow() + private val featuredPackInternal = MutableStateFlow<StarterPackEvent?>(null) + val featuredPack: StateFlow<StarterPackEvent?> = featuredPackInternal.asStateFlow() init { // Recompute featured on cache or shuffle change. @@ -147,7 +147,7 @@ class FollowPacksState( shuffleTickInternal.value++ } - private fun pickRandom(packs: List<FollowListEvent>): FollowListEvent? { + private fun pickRandom(packs: List<StarterPackEvent>): StarterPackEvent? { if (packs.isEmpty()) return null val candidates = packs.filter { it.address().toValue() !in recentlyShown } val pool = if (candidates.isEmpty()) packs else candidates @@ -182,7 +182,7 @@ class FollowPacksState( relayManager.subscribeOnRelay( relay = relay, subId = "$SUB_ID-${relay.url.hashCode()}", - filters = listOf(Filter(kinds = listOf(FollowListEvent.KIND), limit = 200)), + filters = listOf(Filter(kinds = listOf(StarterPackEvent.KIND), limit = 200)), onEvent = { event, r -> cache.consume(event, r) }, ) // Suppress unused variable warning @@ -193,5 +193,5 @@ class FollowPacksState( } /** Returns hashtags present on this pack, lowercase + deduped. */ - fun hashtagsFor(pack: FollowListEvent): List<String> = pack.hashtags().map { it.lowercase() }.distinct() + fun hashtagsFor(pack: StarterPackEvent): List<String> = pack.hashtags().map { it.lowercase() }.distinct() } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/DiscoverScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/DiscoverScreen.kt index 13a34faf40..ad675bc830 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/DiscoverScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/DiscoverScreen.kt @@ -71,7 +71,7 @@ import com.vitorpamplona.amethyst.desktop.model.DesktopIAccount import com.vitorpamplona.amethyst.desktop.network.RelayConnectionManager import com.vitorpamplona.amethyst.desktop.ui.ZapFeedback import com.vitorpamplona.amethyst.desktop.ui.live.LivesSection -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import kotlinx.coroutines.launch import java.awt.Toolkit import java.awt.datatransfer.StringSelection @@ -280,7 +280,7 @@ fun DiscoverScreen( @Composable private fun MiniPackThumb( - pack: FollowListEvent, + pack: StarterPackEvent, onClick: () -> Unit, ) { val title = pack.title()?.ifBlank { null } ?: "Untitled pack" diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowAllConfirmDialog.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowAllConfirmDialog.kt index d5ffe5d4e5..16f8d6b122 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowAllConfirmDialog.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowAllConfirmDialog.kt @@ -42,7 +42,7 @@ import com.vitorpamplona.amethyst.desktop.followpacks.BulkFollowAction import com.vitorpamplona.amethyst.desktop.followpacks.BulkFollowPreview import com.vitorpamplona.amethyst.desktop.model.DesktopIAccount import com.vitorpamplona.amethyst.desktop.network.RelayConnectionManager -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -54,7 +54,7 @@ import kotlinx.coroutines.launch */ @Composable fun FollowAllConfirmDialog( - pack: FollowListEvent, + pack: StarterPackEvent, iAccount: DesktopIAccount, cache: DesktopLocalCache, relayManager: RelayConnectionManager, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackBrowseAllScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackBrowseAllScreen.kt index 60ed6c4164..4dfd5696ad 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackBrowseAllScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackBrowseAllScreen.kt @@ -53,7 +53,7 @@ import com.vitorpamplona.amethyst.desktop.followpacks.FollowPackEditor import com.vitorpamplona.amethyst.desktop.followpacks.FollowPacksState import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent /** * Search-all view for follow packs. @@ -138,10 +138,10 @@ fun FollowPackBrowseAllScreen( } private fun filter( - packs: List<FollowListEvent>, + packs: List<StarterPackEvent>, raw: String, cache: DesktopLocalCache, -): List<FollowListEvent> { +): List<StarterPackEvent> { val q = raw.lowercase() val asHex: HexKey? = decodePublicKeyAsHexOrNull(raw) return packs.filter { pack -> diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackComponents.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackComponents.kt index a3c78d9a1e..5a0524f2ab 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackComponents.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackComponents.kt @@ -62,7 +62,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.ui.components.UserAvatar import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent /** * Stack of overlapping circular avatars for pack members. @@ -143,7 +143,7 @@ fun FollowPackAvatarStack( */ @Composable fun FollowPackHeroCard( - pack: FollowListEvent, + pack: StarterPackEvent, cache: DesktopLocalCache, onFollowAll: () -> Unit, onShare: () -> Unit, @@ -284,7 +284,7 @@ fun FollowPackChipRail( */ @Composable fun FollowPackRow( - pack: FollowListEvent, + pack: StarterPackEvent, cache: DesktopLocalCache, onClick: () -> Unit, modifier: Modifier = Modifier, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackDetailScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackDetailScreen.kt index ae07b1d85d..b7f82a76b3 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackDetailScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackDetailScreen.kt @@ -68,7 +68,7 @@ import com.vitorpamplona.amethyst.desktop.followpacks.subscribeMetadataFor import com.vitorpamplona.amethyst.desktop.model.DesktopIAccount import com.vitorpamplona.amethyst.desktop.network.RelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import kotlinx.coroutines.launch /** @@ -94,7 +94,7 @@ fun FollowPackDetailScreen( @Suppress("UNUSED_VARIABLE") val metadataVersion by cache.metadataVersion.collectAsState() - val pack: FollowListEvent? = + val pack: StarterPackEvent? = remember(addressTag, allPacks) { allPacks.firstOrNull { FollowPackEditor.aTag(it) == addressTag } } @@ -208,7 +208,7 @@ fun FollowPackDetailScreen( @Composable private fun PackHeroDetail( - pack: FollowListEvent, + pack: StarterPackEvent, cache: DesktopLocalCache, onFollowAll: () -> Unit, onShare: () -> Unit, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/RenderFollowPackCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/RenderFollowPackCard.kt index 3a1bc3f9b6..255e223ef7 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/RenderFollowPackCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/RenderFollowPackCard.kt @@ -58,7 +58,7 @@ import com.vitorpamplona.amethyst.desktop.model.DesktopIAccount import com.vitorpamplona.amethyst.desktop.network.RelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress as NAddressEntity /** @@ -81,7 +81,7 @@ fun RenderFollowPackCard( val packVersion by cache.followPackVersion.collectAsState() val aTag = remember(address) { Address.assemble(address.kind, address.author, address.dTag) } - val pack: FollowListEvent? = + val pack: StarterPackEvent? = remember(packVersion, aTag) { cache.snapshotFollowPacks().firstOrNull { FollowPackEditor.aTag(it) == aTag } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopAccountRelays.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopAccountRelays.kt index 6a911cb6af..ef8963f466 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopAccountRelays.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopAccountRelays.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent @@ -45,7 +45,7 @@ import java.util.prefs.Preferences * simpler relay management. * * On Android, Account.dmRelays aggregates: - * - DmRelayListState (ChatMessageRelayListEvent, kind 10050) + * - DmRelayListState (DmRelayListEvent, kind 10050) * - Nip65RelayListState (NIP-65 advertised relays) * - PrivateStorageRelayListState * - LocalRelayListState @@ -119,8 +119,8 @@ class DesktopAccountRelays( private fun loadFromPersistence() { // Load DM relays from event or URL cache - val dmEvent = loadEvent(ChatMessageRelayListEvent.KIND) - if (dmEvent is ChatMessageRelayListEvent) { + val dmEvent = loadEvent(DmRelayListEvent.KIND) + if (dmEvent is DmRelayListEvent) { _dmRelayList.value = dmEvent.relays().toSet() lastDmCreatedAt.set(dmEvent.createdAt) } else { @@ -160,8 +160,8 @@ class DesktopAccountRelays( true } - ChatMessageRelayListEvent.KIND -> { - if (event is ChatMessageRelayListEvent && event.createdAt >= lastDmCreatedAt.get()) { + DmRelayListEvent.KIND -> { + if (event is DmRelayListEvent && event.createdAt >= lastDmCreatedAt.get()) { lastDmCreatedAt.set(event.createdAt) val relays = event.relays().toSet() _dmRelayList.value = relays diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopDmRelayState.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopDmRelayState.kt index 3a193f95ec..9bd76f9ef6 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopDmRelayState.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopDmRelayState.kt @@ -33,7 +33,7 @@ import kotlinx.coroutines.flow.stateIn * Desktop equivalent of DmInboxRelayState. * * Aggregates DM inbox relays from multiple sources: - * - DM relay list (NIP-17 ChatMessageRelayListEvent, kind 10050) + * - DM relay list (NIP-17 DmRelayListEvent, kind 10050) * - Connected relays (fallback when no DM-specific relays are configured) * * On Android, the full Account class manages four relay sources: diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopHiddenUsersState.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopHiddenUsersState.kt index 81356eadc7..b298746177 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopHiddenUsersState.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopHiddenUsersState.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.commons.model.nip51Lists.muteList.MuteListDecr import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleListDecryptionCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.utils.DualCase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -43,7 +43,7 @@ import kotlinx.coroutines.flow.stateIn * Desktop mute/block state holder. * * Assembles the user's NIP-51 mute list (kind 10000, [MuteListEvent]) and the - * legacy block people list (kind 30000 `d=mute`, [PeopleListEvent]) into a single + * legacy block people list (kind 30000 `d=mute`, [FollowSetEvent]) into a single * [LiveHiddenUsers] value that [com.vitorpamplona.amethyst.desktop.model.DesktopIAccount] * feeds to [com.vitorpamplona.amethyst.commons.model.Note.isHiddenFor]. * @@ -69,7 +69,7 @@ class DesktopHiddenUsersState( // Strong refs so the GC keeps these addressable notes (and their decrypt caches) alive. private val muteListNote = cache.getOrCreateAddressableNote(MuteListEvent.createAddress(signer.pubKey)) - private val blockListNote = cache.getOrCreateAddressableNote(PeopleListEvent.createBlockAddress(signer.pubKey)) + private val blockListNote = cache.getOrCreateAddressableNote(FollowSetEvent.createBlockAddress(signer.pubKey)) /** Session-only user hides (e.g. "hide this spammer" without persisting a mute). */ val transientHiddenUsers = MutableStateFlow<Set<String>>(emptySet()) @@ -79,7 +79,7 @@ class DesktopHiddenUsersState( private suspend fun assemble( muteEvent: MuteListEvent?, - blockEvent: PeopleListEvent?, + blockEvent: FollowSetEvent?, transient: Set<String>, showSensitive: Boolean?, ): LiveHiddenUsers { @@ -118,7 +118,7 @@ class DesktopHiddenUsersState( ) { muteState, blockState, transient, showSensitive -> assemble( muteState.note.event as? MuteListEvent, - blockState.note.event as? PeopleListEvent, + blockState.note.event as? FollowSetEvent, transient, showSensitive, ) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopIAccount.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopIAccount.kt index e68f95c233..b1c29e32ee 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopIAccount.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopIAccount.kt @@ -46,12 +46,12 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip17Dm.NIP17Factory import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent @@ -222,9 +222,9 @@ class DesktopIAccount( override val nip47SignerState: INwcSignerState = object : INwcSignerState { - override suspend fun decryptResponse(event: LnZapPaymentResponseEvent): Response? = null + override suspend fun decryptResponse(event: NwcResponseEvent): Response? = null - override suspend fun decryptRequest(event: LnZapPaymentRequestEvent): Request? = null + override suspend fun decryptRequest(event: NwcRequestEvent): Request? = null override fun isNIP47Author(pubKey: String?): Boolean = false } @@ -249,7 +249,7 @@ class DesktopIAccount( return !note.isHiddenFor(hiddenUsersState.flow.value) } - override suspend fun sendNip04PrivateMessage(eventTemplate: EventTemplate<PrivateDmEvent>) { + override suspend fun sendNip04PrivateMessage(eventTemplate: EventTemplate<EncryptedDmEvent>) { if (!isWriteable()) return val signedEvent = signer.sign(eventTemplate) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopRelayListMutator.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopRelayListMutator.kt index d704b2b7d6..07dfb77539 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopRelayListMutator.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopRelayListMutator.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.commons.relays.health.RelayRemovalResult import com.vitorpamplona.amethyst.desktop.network.RelayConnectionManager import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import kotlinx.coroutines.async @@ -80,7 +80,7 @@ class DesktopRelayListMutator( async { runCatching { val newRelays = (dmRelays - url).toList() - val event = ChatMessageRelayListEvent.create(newRelays, signer) + val event = DmRelayListEvent.create(newRelays, signer) accountRelays.consumePublishedEvent(event) relayManager.broadcastToAll(event) }.fold(onSuccess = { null }, onFailure = { RelayListKind.DmInbox }) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nwc/NwcPaymentHandler.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nwc/NwcPaymentHandler.kt index 4fcd463061..514d85b0b2 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nwc/NwcPaymentHandler.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/nwc/NwcPaymentHandler.kt @@ -29,8 +29,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip47WalletConnect.Nip47Client import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceSuccessResponse import com.vitorpamplona.quartz.nip47WalletConnect.rpc.IErrorResponseLike import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceSuccessResponse @@ -93,7 +93,7 @@ class NwcPaymentHandler( // Create payment request event val requestEvent = - LnZapPaymentRequestEvent.create( + NwcRequestEvent.create( lnInvoice = bolt11, walletServicePubkey = nwcConnection.pubKeyHex, signer = nwcSigner, @@ -126,7 +126,7 @@ class NwcPaymentHandler( suspendCancellableCoroutine { continuation -> val filter = Filter( - kinds = listOf(LnZapPaymentResponseEvent.KIND), + kinds = listOf(NwcResponseEvent.KIND), authors = listOf(nwcConnection.pubKeyHex), tags = mapOf("e" to listOf(requestId)), ) @@ -138,7 +138,7 @@ class NwcPaymentHandler( subId = subId, filters = listOf(filter), onEvent = { event, relay -> - if (event is LnZapPaymentResponseEvent && event.requestId() == requestId) { + if (event is NwcResponseEvent && event.requestId() == requestId) { // Move verify + cache mutation + decrypt off the relay's // WebSocket reader thread; Schnorr verify is non-trivial // CPU work and shouldn't block frame parsing. @@ -313,7 +313,7 @@ class NwcPaymentHandler( suspendCancellableCoroutine { continuation -> val filter = Filter( - kinds = listOf(LnZapPaymentResponseEvent.KIND), + kinds = listOf(NwcResponseEvent.KIND), authors = listOf(nwcConnection.pubKeyHex), tags = mapOf("e" to listOf(requestId)), ) @@ -325,7 +325,7 @@ class NwcPaymentHandler( subId = subId, filters = listOf(filter), onEvent = { event, _ -> - if (event is LnZapPaymentResponseEvent && event.requestId() == requestId) { + if (event is NwcResponseEvent && event.requestId() == requestId) { @OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class) kotlinx.coroutines.GlobalScope.launch(kotlinx.coroutines.Dispatchers.IO) { if (!localCache.justVerify(event)) return@launch diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt index 78c1cffa71..d72846949e 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt @@ -310,7 +310,7 @@ class DesktopRelaySubscriptionsCoordinator( ), // Zap receipts (kind 9735) targeting these notes Filter( - kinds = listOf(com.vitorpamplona.quartz.nip57Zaps.LnZapEvent.KIND), + kinds = listOf(com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent.KIND), tags = mapOf("e" to noteIds), ), // Reposts (kind 6) targeting these notes diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterBuilders.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterBuilders.kt index 525013809f..0b2d354bdd 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterBuilders.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterBuilders.kt @@ -447,7 +447,7 @@ object FilterBuilders { limit: Int? = null, ): Filter = Filter( - kinds = listOf(9735), // LnZapEvent.KIND + kinds = listOf(9735), // ZapReceiptEvent.KIND tags = mapOf("e" to eventIds), limit = limit, ) @@ -517,7 +517,7 @@ object FilterBuilders { since: Long? = null, ): Filter = Filter( - kinds = listOf(4), // PrivateDmEvent.KIND + kinds = listOf(4), // EncryptedDmEvent.KIND tags = mapOf("p" to listOf(pubKeyHex)), limit = limit, since = since, @@ -537,7 +537,7 @@ object FilterBuilders { since: Long? = null, ): Filter = Filter( - kinds = listOf(4), // PrivateDmEvent.KIND + kinds = listOf(4), // EncryptedDmEvent.KIND authors = listOf(pubKeyHex), limit = limit, since = since, @@ -569,7 +569,7 @@ object FilterBuilders { */ fun dmRelayList(pubKeyHex: String): Filter = Filter( - kinds = listOf(10050), // ChatMessageRelayListEvent.KIND + kinds = listOf(10050), // DmRelayListEvent.KIND authors = listOf(pubKeyHex), limit = 1, ) @@ -588,7 +588,7 @@ object FilterBuilders { until: Long? = null, ): Filter = Filter( - kinds = listOf(30023), // LongTextNoteEvent.KIND + kinds = listOf(30023), // LongFormContentEvent.KIND limit = limit, since = since, until = until, @@ -606,7 +606,7 @@ object FilterBuilders { dTag: String, ): Filter = Filter( - kinds = listOf(30023), // LongTextNoteEvent.KIND + kinds = listOf(30023), // LongFormContentEvent.KIND authors = listOf(pubkey), tags = mapOf("d" to listOf(dTag)), limit = 1, @@ -628,7 +628,7 @@ object FilterBuilders { until: Long? = null, ): Filter = Filter( - kinds = listOf(30023), // LongTextNoteEvent.KIND + kinds = listOf(30023), // LongFormContentEvent.KIND authors = authors, limit = limit, since = since, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterDMs.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterDMs.kt index 9ab82738b8..e9f52e6115 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterDMs.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterDMs.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent @@ -50,7 +50,7 @@ object FilterDMs { limit: Int? = null, ): Filter = Filter( - kinds = listOf(PrivateDmEvent.KIND), + kinds = listOf(EncryptedDmEvent.KIND), tags = mapOf("p" to listOf(userPubKeyHex)), since = since, limit = limit, @@ -70,7 +70,7 @@ object FilterDMs { limit: Int? = null, ): Filter = Filter( - kinds = listOf(PrivateDmEvent.KIND), + kinds = listOf(EncryptedDmEvent.KIND), authors = listOf(userPubKeyHex), since = since, limit = limit, @@ -96,14 +96,14 @@ object FilterDMs { return listOf( // Messages TO me from conversation participants Filter( - kinds = listOf(PrivateDmEvent.KIND), + kinds = listOf(EncryptedDmEvent.KIND), authors = conversationPubKeys.toList(), tags = mapOf("p" to listOf(userPubKeyHex)), since = since, ), // Messages FROM me to conversation participants Filter( - kinds = listOf(PrivateDmEvent.KIND), + kinds = listOf(EncryptedDmEvent.KIND), authors = listOf(userPubKeyHex), tags = mapOf("p" to conversationPubKeys.toList()), since = since, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ArticleReaderScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ArticleReaderScreen.kt index 8092de6e4e..4a97e66179 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ArticleReaderScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ArticleReaderScreen.kt @@ -91,11 +91,11 @@ import com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription import com.vitorpamplona.amethyst.desktop.ui.highlights.ArticleHighlightsPanel import com.vitorpamplona.amethyst.desktop.ui.highlights.HighlightAnnotationDialog import com.vitorpamplona.quartz.nip18Reposts.RepostEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import kotlinx.coroutines.launch import java.text.DateFormat import java.util.Date @@ -144,7 +144,7 @@ fun ArticleReaderScreen( val dTag = parsed?.third // Article state - var article by remember(addressTag) { mutableStateOf<LongTextNoteEvent?>(null) } + var article by remember(addressTag) { mutableStateOf<LongFormContentEvent?>(null) } var eoseReceived by remember(addressTag) { mutableStateOf(false) } // Zoom level for article text @@ -212,7 +212,7 @@ fun ArticleReaderScreen( filters = listOf(FilterBuilders.longFormByAddress(pubkey, dTag)), relays = configuredRelays, onEvent = { event, _, _, _ -> - if (event is LongTextNoteEvent) { + if (event is LongFormContentEvent) { // Keep the most recent version val current = article if (current == null || event.createdAt > current.createdAt) { @@ -246,7 +246,7 @@ fun ArticleReaderScreen( relays = configuredRelays, eventIds = eventIds, onEvent = { event, _, _, _ -> - if (event is LnZapEvent) { + if (event is ZapReceiptEvent) { val receipt = event.toZapReceipt(localCache) ?: return@createZapsSubscription if (zapReceipts.none { it.createdAt == receipt.createdAt && it.senderPubKey == receipt.senderPubKey }) { zapReceipts = zapReceipts + receipt diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt index 34cdbcfae5..9d4c62415b 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt @@ -102,7 +102,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip88Polls.poll.tags.PollType import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent @@ -168,9 +168,9 @@ data class ZapReceipt( ) /** - * Converts an LnZapEvent to a ZapReceipt for display. + * Converts an ZapReceiptEvent to a ZapReceipt for display. */ -fun LnZapEvent.toZapReceipt(localCache: DesktopLocalCache): ZapReceipt? { +fun ZapReceiptEvent.toZapReceipt(localCache: DesktopLocalCache): ZapReceipt? { val senderPubKey = zappedRequestAuthor() ?: return null val amountSats = amount?.toLong() ?: return null @@ -484,7 +484,7 @@ fun ZapReceiptsPopup( val name = user?.toBestDisplayName() ?: pubKey.take(12) val pictureUrl = user?.profilePicture() val amount = - (receipt?.event as? LnZapEvent)?.amount?.toLong() + (receipt?.event as? ZapReceiptEvent)?.amount?.toLong() ?: return@mapNotNull null val message = request.event?.content?.ifBlank { null } ZapEntry(pubKey, pictureUrl, name, amount, message) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt index 4bdb8f1083..3791bfcfbf 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt @@ -87,7 +87,7 @@ import com.vitorpamplona.amethyst.desktop.ui.notifications.NotificationGroup import com.vitorpamplona.amethyst.desktop.ui.notifications.groupNotifications import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent @@ -97,7 +97,7 @@ import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent @@ -157,7 +157,7 @@ sealed class NotificationItem( val NotificationItem.effectiveAuthorPubKey: String get() = when (val e = event) { - is LnZapEvent -> e.zapRequest?.pubKey ?: e.pubKey + is ZapReceiptEvent -> e.zapRequest?.pubKey ?: e.pubKey else -> event.pubKey } @@ -169,7 +169,7 @@ private fun classifyNotification(event: Event): NotificationItem? = when (event) { is ReactionEvent -> NotificationItem.Reaction(event, event.createdAt, event.content) is RepostEvent, is GenericRepostEvent -> NotificationItem.Repost(event, event.createdAt) - is LnZapEvent -> NotificationItem.Zap(event, event.createdAt, event.amount?.toLong()) + is ZapReceiptEvent -> NotificationItem.Zap(event, event.createdAt, event.amount?.toLong()) // Nutzaps: treat like a zap; sats amount extraction requires the Cashu // token proof and is deferred to when Desktop renders zap detail. is NutzapEvent -> NotificationItem.Zap(event, event.createdAt, null) @@ -186,7 +186,7 @@ private fun classifyNotification(event: Event): NotificationItem? = // NIP-28 channel messages read like public mentions. is ChannelMessageEvent -> NotificationItem.Mention(event, event.createdAt) // DMs (NIP-04 legacy + NIP-17 gift-wrap + rumor + file-header). - is PrivateDmEvent, + is EncryptedDmEvent, is ChatMessageEvent, is GiftWrapEvent, is ChatMessageEncryptedFileHeaderEvent, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ReadsScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ReadsScreen.kt index 96a44b0cee..42fd5cfb90 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ReadsScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ReadsScreen.kt @@ -67,7 +67,7 @@ import com.vitorpamplona.amethyst.desktop.subscriptions.createFollowingLongFormF import com.vitorpamplona.amethyst.desktop.subscriptions.createLongFormFeedSubscription import com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import java.text.DateFormat import java.util.Date @@ -83,7 +83,7 @@ private fun formatDate(timestamp: Long): String = dateFormat.format(Date(timesta */ @Composable fun LongFormCard( - event: LongTextNoteEvent, + event: LongFormContentEvent, localCache: DesktopLocalCache, onAuthorClick: (String) -> Unit = {}, onClick: () -> Unit = {}, @@ -185,9 +185,9 @@ fun ReadsScreen( val eventState = remember { - EventCollectionState<LongTextNoteEvent>( + EventCollectionState<LongFormContentEvent>( getId = { it.id }, - sortComparator = compareByDescending<LongTextNoteEvent> { it.publishedAt() ?: it.createdAt }.thenBy { it.id }, + sortComparator = compareByDescending<LongFormContentEvent> { it.publishedAt() ?: it.createdAt }.thenBy { it.id }, maxSize = 100, scope = scope, ) @@ -204,10 +204,10 @@ fun ReadsScreen( LaunchedEffect(Unit) { val cached = localCache.notes.filterIntoSet { _, note -> - note.event is LongTextNoteEvent + note.event is LongFormContentEvent } cached.forEach { note -> - (note.event as? LongTextNoteEvent)?.let { eventState.addItem(it) } + (note.event as? LongFormContentEvent)?.let { eventState.addItem(it) } } if (cached.isNotEmpty()) eoseReceivedCount++ } @@ -249,7 +249,7 @@ fun ReadsScreen( relays = connectedRelays, onEvent = { event, _, relay, _ -> subscriptionsCoordinator?.consumeEvent(event, relay) - if (event is LongTextNoteEvent) { + if (event is LongFormContentEvent) { eventState.addItem(event) } }, @@ -266,7 +266,7 @@ fun ReadsScreen( followedUsers = followedUsers.toList(), onEvent = { event, _, relay, _ -> subscriptionsCoordinator?.consumeEvent(event, relay) - if (event is LongTextNoteEvent) { + if (event is LongFormContentEvent) { eventState.addItem(event) } }, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt index bf08ddbbc2..7264e2d71b 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt @@ -117,7 +117,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip19Bech32.toNpub -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity @@ -125,9 +125,9 @@ import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.identityClaims import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent @@ -231,7 +231,7 @@ fun UserProfileScreen( var showProfileReportDialog by remember(pubKeyHex) { mutableStateOf(false) } var showAddToListMenu by remember(pubKeyHex) { mutableStateOf(false) } val followPacksState = LocalFollowPacksState.current - val followPacks by (followPacksState?.allPacks ?: MutableStateFlow(emptyList<FollowListEvent>())).collectAsState() + val followPacks by (followPacksState?.allPacks ?: MutableStateFlow(emptyList<StarterPackEvent>())).collectAsState() val profileHidden by (iAccount?.hiddenUsers ?: kotlinx.coroutines.flow.MutableStateFlow(com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers.EMPTY)).collectAsState() val isUserMuted = profileHidden.isUserHidden(pubKeyHex) @@ -351,7 +351,7 @@ fun UserProfileScreen( var selectedTab by remember { mutableStateOf(0) } var lightboxState by remember { mutableStateOf<LightboxState?>(null) } val pictureEvents = remember { mutableStateListOf<PictureEvent>() } - val articleEvents = remember { mutableStateListOf<LongTextNoteEvent>() } + val articleEvents = remember { mutableStateListOf<LongFormContentEvent>() } val highlightEvents = remember { mutableStateListOf<HighlightEvent>() } // Followers / Following user lists (pubkeys) and the profile's relay list. @@ -361,7 +361,7 @@ fun UserProfileScreen( var relayList by remember(pubKeyHex) { mutableStateOf<List<Triple<String, Boolean, Boolean>>>(emptyList()) } // Zaps received: raw zap receipts, aggregated per zapper (pubkey → total sats). - val zapEvents = remember(pubKeyHex) { mutableStateListOf<LnZapEvent>() } + val zapEvents = remember(pubKeyHex) { mutableStateListOf<ZapReceiptEvent>() } var zapAmounts by remember(pubKeyHex) { mutableStateOf<List<Pair<String, BigDecimal>>>(emptyList()) } var zapTotalSats by remember(pubKeyHex) { mutableStateOf(BigDecimal.ZERO) } @@ -559,13 +559,13 @@ fun UserProfileScreen( listOf( FilterBuilders.byAuthors( authors = listOf(pubKeyHex), - kinds = listOf(LongTextNoteEvent.KIND), + kinds = listOf(LongFormContentEvent.KIND), limit = 50, ), ), relays = connectedRelays, onEvent = { event, _, _, _ -> - if (event is LongTextNoteEvent && articleEvents.none { it.id == event.id }) { + if (event is LongFormContentEvent && articleEvents.none { it.id == event.id }) { articleEvents.add(event) } }, @@ -710,13 +710,13 @@ fun UserProfileScreen( listOf( FilterBuilders.byPTags( pubKeys = listOf(pubKeyHex), - kinds = listOf(LnZapEvent.KIND), + kinds = listOf(ZapReceiptEvent.KIND), limit = 500, ), ), relays = connectedRelays, onEvent = { event, _, _, _ -> - if (event is LnZapEvent && zapEvents.none { it.id == event.id }) { + if (event is ZapReceiptEvent && zapEvents.none { it.id == event.id }) { zapEvents.add(event) } }, @@ -971,7 +971,7 @@ fun UserProfileScreen( scope.launch { try { val updated = - FollowListEvent.add( + StarterPackEvent.add( pack, UserTag(pubKeyHex, null), acct.signer, @@ -1447,7 +1447,7 @@ fun UserProfileScreen( } } else { items( - articleEvents.sortedWith(compareByDescending<LongTextNoteEvent> { it.publishedAt() ?: it.createdAt }.thenBy { it.id }), + articleEvents.sortedWith(compareByDescending<LongFormContentEvent> { it.publishedAt() ?: it.createdAt }.thenBy { it.id }), key = { "art-${it.id}" }, ) { article -> LongFormCard( @@ -1455,7 +1455,7 @@ fun UserProfileScreen( localCache = localCache, onAuthorClick = { onNavigateToProfile(article.pubKey) }, onClick = { - val addressTag = "${LongTextNoteEvent.KIND}:${article.pubKey}:${article.dTag()}" + val addressTag = "${LongFormContentEvent.KIND}:${article.pubKey}:${article.dTag()}" onNavigateToArticle(addressTag) }, ) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/ChatPane.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/ChatPane.kt index 1c3fc89cf5..c34aea979b 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/ChatPane.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/ChatPane.kt @@ -125,7 +125,7 @@ import com.vitorpamplona.amethyst.desktop.ui.media.QualitySelectorChip import com.vitorpamplona.amethyst.desktop.ui.note.DesktopRichText import com.vitorpamplona.amethyst.desktop.ui.note.RichTextCallbacks import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip17Dm.NIP17Factory import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent @@ -714,7 +714,7 @@ private fun MessageWithReactions( LaunchedEffect(note.idHex, event) { decryptedContent = when (event) { - is PrivateDmEvent -> { + is EncryptedDmEvent -> { try { event.decryptContent(account.signer) } catch (_: Exception) { @@ -776,7 +776,7 @@ private fun MessageWithReactions( ) { // Encryption badge when (event) { - is PrivateDmEvent -> { + is EncryptedDmEvent -> { Icon( MaterialSymbols.LockOpen, contentDescription = "NIP-04 (legacy)", @@ -947,7 +947,7 @@ private fun MessageReplyContext( preview = when (val e = parent.event) { is ChatMessageEncryptedFileHeaderEvent -> "📎 Attachment" - is PrivateDmEvent -> + is EncryptedDmEvent -> try { e.decryptContent(account.signer) } catch (_: Exception) { @@ -1076,7 +1076,7 @@ private fun ReplyPreviewBar( preview = when (event) { is ChatMessageEncryptedFileHeaderEvent -> "📎 Attachment" - is PrivateDmEvent -> + is EncryptedDmEvent -> try { event.decryptContent(account.signer) } catch (_: Exception) { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/ChatroomListState.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/ChatroomListState.kt index 2b18254d14..34873597ac 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/ChatroomListState.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/ChatroomListState.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -138,7 +138,7 @@ class ChatroomListState( private suspend fun decryptPreview(event: com.vitorpamplona.quartz.nip01Core.core.Event?): String { if (event == null) return "" return when (event) { - is PrivateDmEvent -> { + is EncryptedDmEvent -> { decryptedContentCache.getOrPut(event.id) { try { event.decryptContent(account.signer) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/composer/GifResult.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/composer/GifResult.kt index f8eaa49707..302b3651dd 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/composer/GifResult.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/composer/GifResult.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.desktop.ui.chats.composer import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchAll import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent /** A single GIF result surfaced from a NIP-94 (kind 1063) metadata event. */ data class GifResult( @@ -47,7 +47,7 @@ suspend fun searchGifs( val trimmed = query.trim() val filter = Filter( - kinds = listOf(FileHeaderEvent.KIND), + kinds = listOf(FileMetadataEvent.KIND), tags = mapOf("m" to listOf("image/gif")), limit = limitPerRelay, search = trimmed.ifBlank { null }, @@ -59,7 +59,7 @@ suspend fun searchGifs( runCatching { client.fetchAll(relay, filter, idleTimeoutMs = 8_000L) } .getOrDefault(emptyList()) for (event in events) { - val fileHeader = event as? FileHeaderEvent ?: continue + val fileHeader = event as? FileMetadataEvent ?: continue val url = fileHeader.url() ?: continue raw.add(GifResult(url = url, description = fileHeader.content)) } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt index 62f67372bb..6c6e49ad14 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt @@ -376,7 +376,7 @@ private fun RenderBechSegment( .NAddress .parse(segment.segmentText) } - if (naddr != null && naddr.kind == com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent.KIND) { + if (naddr != null && naddr.kind == com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent.KIND) { val followPacks = com.vitorpamplona.amethyst.desktop.ui.deck.LocalFollowPacksState.current val relayManager = com.vitorpamplona.amethyst.desktop.ui.deck.LocalRelayManager.current if (localCache != null && relayManager != null && followPacks != null) { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/notifications/DesktopNotificationAutoDispatcher.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/notifications/DesktopNotificationAutoDispatcher.kt index 23981aab22..6ec6fd2e7c 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/notifications/DesktopNotificationAutoDispatcher.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/notifications/DesktopNotificationAutoDispatcher.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.amethyst.commons.moderation.notifications.nowEpochSecon import com.vitorpamplona.amethyst.commons.moderation.notifications.sanitizeForToast import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent @@ -39,7 +39,7 @@ import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent @@ -178,14 +178,14 @@ class DesktopNotificationAutoDispatcher( when (event) { is ReactionEvent -> NotifKind.REACTION is RepostEvent, is GenericRepostEvent -> NotifKind.REPOST - is LnZapEvent, is NutzapEvent, is OnchainZapEvent -> NotifKind.ZAP + is ZapReceiptEvent, is NutzapEvent, is OnchainZapEvent -> NotifKind.ZAP is TextNoteEvent -> { val isReply = event.tags.any { it.size > 1 && it[0] == "e" } if (isReply) NotifKind.REPLY else NotifKind.MENTION } is CommentEvent -> NotifKind.REPLY is ChannelMessageEvent -> NotifKind.MENTION - is PrivateDmEvent, + is EncryptedDmEvent, is ChatMessageEvent, is GiftWrapEvent, is ChatMessageEncryptedFileHeaderEvent, @@ -199,7 +199,7 @@ class DesktopNotificationAutoDispatcher( ): NotificationSpec { val effectivePubKey = when (event) { - is LnZapEvent -> event.zapRequest?.pubKey ?: event.pubKey + is ZapReceiptEvent -> event.zapRequest?.pubKey ?: event.pubKey else -> event.pubKey } val displayName = @@ -207,7 +207,7 @@ class DesktopNotificationAutoDispatcher( ?: "Someone" val amountText = - (event as? LnZapEvent)?.amount?.let { " ${it.toLong() / 1000} sats" } ?: "" + (event as? ZapReceiptEvent)?.amount?.let { " ${it.toLong() / 1000} sats" } ?: "" val title = when (kind) { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/relay/DmRelayEditor.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/relay/DmRelayEditor.kt index e4063ffaf2..86673cc80b 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/relay/DmRelayEditor.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/relay/DmRelayEditor.kt @@ -57,7 +57,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import kotlinx.coroutines.delay import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -181,7 +181,7 @@ fun DmRelayEditor( } scope.launch { try { - val event = ChatMessageRelayListEvent.create(localRelays.toList(), signer) + val event = DmRelayListEvent.create(localRelays.toList(), signer) onPublish(event) onDmRelaysUpdated(localRelays.toSet()) savedMessage = "Published ${localRelays.size} relay(s)" diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt index c225256ee4..3b0fab764f 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt @@ -69,7 +69,7 @@ import com.vitorpamplona.amethyst.desktop.ui.note.NoteCard import com.vitorpamplona.amethyst.desktop.ui.note.SpamCheckedNoteRender import com.vitorpamplona.amethyst.desktop.ui.note.WoTBadge import com.vitorpamplona.amethyst.desktop.ui.rememberDisplayData -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent @Composable @@ -96,9 +96,9 @@ fun SearchResultsList( // Group notes by kind val textNotes = notes.filter { it.kind == 1 } - val articles = notes.filter { it.kind == LongTextNoteEvent.KIND } + val articles = notes.filter { it.kind == LongFormContentEvent.KIND } val polls = notes.filter { it.kind == PollEvent.KIND } - val otherNotes = notes.filter { it.kind != 1 && it.kind != LongTextNoteEvent.KIND && it.kind != PollEvent.KIND } + val otherNotes = notes.filter { it.kind != 1 && it.kind != LongFormContentEvent.KIND && it.kind != PollEvent.KIND } // Per-section collapsed state (absent = expanded) val collapsedSections = remember { mutableStateMapOf<String, Boolean>() } diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopDmRelayListConsumeTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopDmRelayListConsumeTest.kt index 8d0cbd22ce..ee625359b3 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopDmRelayListConsumeTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopDmRelayListConsumeTest.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.desktop.cache import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull @@ -50,7 +50,7 @@ class DesktopDmRelayListConsumeTest { signer: NostrSignerSync, relays: List<NormalizedRelayUrl> = dmInbox, createdAt: Long = 1_700_000_000, - ): ChatMessageRelayListEvent = ChatMessageRelayListEvent.create(relays, signer, createdAt) + ): DmRelayListEvent = DmRelayListEvent.create(relays, signer, createdAt) @Test fun `consume routes kind 10050 into the User model`() { diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/nwc/NwcPaymentHandlerTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/nwc/NwcPaymentHandlerTest.kt index 752deaf576..28ef0d7dc8 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/nwc/NwcPaymentHandlerTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/nwc/NwcPaymentHandlerTest.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceSuccessResponse import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceSuccessResponse import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransaction @@ -157,8 +157,8 @@ class NwcPaymentHandlerTest { // -- Response event structure -- @Test - fun `LnZapPaymentResponseEvent has correct KIND`() { - assertEquals(23195, LnZapPaymentResponseEvent.KIND) + fun `NwcResponseEvent has correct KIND`() { + assertEquals(23195, NwcResponseEvent.KIND) } // -- PayInvoiceSuccessResponse structure -- diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/nwc/NwcRpcIntegrationTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/nwc/NwcRpcIntegrationTest.kt index ce6bf5f8a5..04d1f2e080 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/nwc/NwcRpcIntegrationTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/nwc/NwcRpcIntegrationTest.kt @@ -25,8 +25,8 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip47WalletConnect.Nip47Client -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceMethod import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceSuccessResponse import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceMethod @@ -66,7 +66,7 @@ class NwcRpcIntegrationTest { val requestEvent = client.payInvoice("lnbc50n1pjtest...") // Verify request event structure - assertEquals(LnZapPaymentRequestEvent.KIND, requestEvent.kind) + assertEquals(NwcRequestEvent.KIND, requestEvent.kind) assertTrue(requestEvent.content.isNotBlank()) // Wallet decrypts the request @@ -82,7 +82,7 @@ class NwcRpcIntegrationTest { val requestEvent = client.getBalance() // Verify it's a valid NWC request - assertEquals(LnZapPaymentRequestEvent.KIND, requestEvent.kind) + assertEquals(NwcRequestEvent.KIND, requestEvent.kind) // Wallet decrypts and verifies method type val decrypted = requestEvent.decryptRequest(walletSigner) @@ -90,7 +90,7 @@ class NwcRpcIntegrationTest { // Wallet builds encrypted response val responseEvent = - LnZapPaymentResponseEvent.createResponse( + NwcResponseEvent.createResponse( response = GetBalanceSuccessResponse( result = GetBalanceSuccessResponse.GetBalanceResult(balance = 125_000), @@ -100,7 +100,7 @@ class NwcRpcIntegrationTest { ) // Verify response event structure - assertEquals(LnZapPaymentResponseEvent.KIND, responseEvent.kind) + assertEquals(NwcResponseEvent.KIND, responseEvent.kind) assertEquals(requestEvent.id, responseEvent.requestId()) // Client decrypts response @@ -124,7 +124,7 @@ class NwcRpcIntegrationTest { // Wallet responds with invoice val fakeInvoice = "lnbc500n1pjgenerated..." val responseEvent = - LnZapPaymentResponseEvent.createResponse( + NwcResponseEvent.createResponse( response = MakeInvoiceSuccessResponse( result = @@ -153,7 +153,7 @@ class NwcRpcIntegrationTest { val requestEvent = client.payInvoice("lnbc100n1...") val responseEvent = - LnZapPaymentResponseEvent.createResponse( + NwcResponseEvent.createResponse( response = PayInvoiceSuccessResponse( result = @@ -183,7 +183,7 @@ class NwcRpcIntegrationTest { // Build a request and verify it works val requestEvent = client.getBalance() - assertEquals(LnZapPaymentRequestEvent.KIND, requestEvent.kind) + assertEquals(NwcRequestEvent.KIND, requestEvent.kind) // Wallet can decrypt it val decrypted = requestEvent.decryptRequest(walletSigner) diff --git a/geode/src/test/kotlin/com/vitorpamplona/geode/ConcordPlaneKeyDeletionTest.kt b/geode/src/test/kotlin/com/vitorpamplona/geode/ConcordPlaneKeyDeletionTest.kt index 15c5fcd7a1..051c1736f8 100644 --- a/geode/src/test/kotlin/com/vitorpamplona/geode/ConcordPlaneKeyDeletionTest.kt +++ b/geode/src/test/kotlin/com/vitorpamplona/geode/ConcordPlaneKeyDeletionTest.kt @@ -34,7 +34,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent import com.vitorpamplona.quartz.utils.RandomInstance import com.vitorpamplona.quartz.utils.TimeUtils @@ -177,7 +177,7 @@ class ConcordPlaneKeyDeletionTest { // The banned member still derives `plane`, and every wrap above IS authored by it — so // this kind-5 satisfies a same-author check. It must still be refused. - val deletion = planeSigner().sign(DeletionEvent.build(history, createdAt = now + 10)) + val deletion = planeSigner().sign(DeletionRequestEvent.build(history, createdAt = now + 10)) assertEquals(true, client.publishAndConfirm(deletion, setOf(relayUrl)), "the relay accepts the event itself") assertEquals( @@ -230,7 +230,7 @@ class ConcordPlaneKeyDeletionTest { assertEquals(true, client.publishAndConfirm(addressedWrap, setOf(relayUrl))) assertEquals(1, query(Filter(ids = listOf(addressedWrap.id))).size) - val deletion = mallory.sign(DeletionEvent.build(listOf(addressedWrap), createdAt = now + 1)) + val deletion = mallory.sign(DeletionRequestEvent.build(listOf(addressedWrap), createdAt = now + 1)) assertEquals(true, client.publishAndConfirm(deletion, setOf(relayUrl))) assertEquals( diff --git a/geode/src/test/kotlin/com/vitorpamplona/geode/DeletionSettleBenchmark.kt b/geode/src/test/kotlin/com/vitorpamplona/geode/DeletionSettleBenchmark.kt index 46e4b7a1ca..5f229740d9 100644 --- a/geode/src/test/kotlin/com/vitorpamplona/geode/DeletionSettleBenchmark.kt +++ b/geode/src/test/kotlin/com/vitorpamplona/geode/DeletionSettleBenchmark.kt @@ -29,7 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.negentropySet import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventStore -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.runBlocking @@ -81,7 +81,7 @@ class DeletionSettleBenchmark : RelayClientTest() { // The last K are the ones we deleted locally. val deleted = notes.takeLast(k) val kept = notes.dropLast(k) - val deletions = deleted.map { signer.sign(DeletionEvent.build(listOf(it), createdAt = it.createdAt + 1)) } + val deletions = deleted.map { signer.sign(DeletionRequestEvent.build(listOf(it), createdAt = it.createdAt + 1)) } // Relay holds all N notes; we hold the N-K we didn't delete, plus the K kind-5s. defaultRelay.preload(notes) diff --git a/geode/src/test/kotlin/com/vitorpamplona/geode/DeletionSyncTest.kt b/geode/src/test/kotlin/com/vitorpamplona/geode/DeletionSyncTest.kt index f3a7ef4306..22b07e8be7 100644 --- a/geode/src/test/kotlin/com/vitorpamplona/geode/DeletionSyncTest.kt +++ b/geode/src/test/kotlin/com/vitorpamplona/geode/DeletionSyncTest.kt @@ -35,7 +35,7 @@ import com.vitorpamplona.quartz.nip01Core.store.IdAndTime import com.vitorpamplona.quartz.nip01Core.store.deletionsCovering import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventStore import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent import kotlinx.coroutines.runBlocking @@ -69,7 +69,7 @@ class DeletionSyncTest : RelayClientTest() { fun idBasedDeletionCoversByETag() = runBlocking { val target = note("delete me") - val deletion = signer.sign(DeletionEvent.build(listOf(target), createdAt = target.createdAt + 1)) + val deletion = signer.sign(DeletionRequestEvent.build(listOf(target), createdAt = target.createdAt + 1)) store.insert(deletion) assertEquals(listOf(deletion.id), store.deletionsCovering(listOf(target), here).map { it.id }) @@ -82,7 +82,7 @@ class DeletionSyncTest : RelayClientTest() { runBlocking { val contacts = ContactListEvent.createFromScratch(emptyList(), null, signer) // Address-only deletion (no `e` tag) → only the `a`-tag path can match it. - val delAddr = signer.sign(DeletionEvent.buildAddressOnly(listOf(contacts), createdAt = contacts.createdAt + 1)) + val delAddr = signer.sign(DeletionRequestEvent.buildAddressOnly(listOf(contacts), createdAt = contacts.createdAt + 1)) store.insert(delAddr) assertEquals( @@ -93,7 +93,7 @@ class DeletionSyncTest : RelayClientTest() { // NIP-09 cutoff: a deletion OLDER than the event does not delete it. val stale = EventStore(null) - stale.insert(signer.sign(DeletionEvent.buildAddressOnly(listOf(contacts), createdAt = contacts.createdAt - 1))) + stale.insert(signer.sign(DeletionRequestEvent.buildAddressOnly(listOf(contacts), createdAt = contacts.createdAt - 1))) assertTrue(stale.deletionsCovering(listOf(contacts), here).isEmpty(), "an older address deletion does not cover") stale.close() } @@ -129,7 +129,7 @@ class DeletionSyncTest : RelayClientTest() { fun sendsCoveringDeletionSoRelayRemovesTheNote() = runBlocking { val target = note("delete me e2e") - val deletion = signer.sign(DeletionEvent.build(listOf(target), createdAt = target.createdAt + 1)) + val deletion = signer.sign(DeletionRequestEvent.build(listOf(target), createdAt = target.createdAt + 1)) // Relay holds the note; we already deleted it locally (hold only the kind-5). defaultRelay.preload(listOf(target)) @@ -163,7 +163,7 @@ class DeletionSyncTest : RelayClientTest() { fun appliesRelaysDeletionSoLocalRemovesTheNote() = runBlocking { val target = note("delete me down") - val deletion = signer.sign(DeletionEvent.build(listOf(target), createdAt = target.createdAt + 1)) + val deletion = signer.sign(DeletionRequestEvent.build(listOf(target), createdAt = target.createdAt + 1)) // Relay already applied the deletion → holds only the kind-5. defaultRelay.preload(listOf(target, deletion)) @@ -190,7 +190,7 @@ class DeletionSyncTest : RelayClientTest() { val ourEvents = local.store.query<Event>(Filter(ids = diff.haveIds)) val relayDeletions = deletionsCovering(ourEvents, defaultRelayUrl) { f -> defaultRelay.store.query<Event>(f) } assertEquals(listOf(deletion.id), relayDeletions.map { it.id }) - relayDeletions.filterIsInstance<DeletionEvent>().forEach { local.store.insert(it) } + relayDeletions.filterIsInstance<DeletionRequestEvent>().forEach { local.store.insert(it) } assertTrue( local.store.query<Event>(Filter(ids = listOf(target.id))).isEmpty(), @@ -206,7 +206,7 @@ class DeletionSyncTest : RelayClientTest() { fun settleSendsOurDeletionUp() = runBlocking { val target = note("settle up") - val deletion = signer.sign(DeletionEvent.build(listOf(target), createdAt = target.createdAt + 1)) + val deletion = signer.sign(DeletionRequestEvent.build(listOf(target), createdAt = target.createdAt + 1)) val localStore = EventStore(null) localStore.insert(target) localStore.insert(deletion) // deletes target locally, keeps the kind-5 @@ -239,7 +239,7 @@ class DeletionSyncTest : RelayClientTest() { fun settleAppliesRelayDeletionDown() = runBlocking { val target = note("settle down") - val deletion = signer.sign(DeletionEvent.build(listOf(target), createdAt = target.createdAt + 1)) + val deletion = signer.sign(DeletionRequestEvent.build(listOf(target), createdAt = target.createdAt + 1)) defaultRelay.preload(listOf(target, deletion)) // relay deletes target, keeps the kind-5 val localStore = EventStore(null) localStore.insert(target) diff --git a/geode/src/test/kotlin/com/vitorpamplona/geode/Nip01ComplianceTest.kt b/geode/src/test/kotlin/com/vitorpamplona/geode/Nip01ComplianceTest.kt index 7f69de4c89..9326acbb7f 100644 --- a/geode/src/test/kotlin/com/vitorpamplona/geode/Nip01ComplianceTest.kt +++ b/geode/src/test/kotlin/com/vitorpamplona/geode/Nip01ComplianceTest.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED import kotlinx.coroutines.runBlocking @@ -350,7 +350,7 @@ class Nip01ComplianceTest : RelayClientTest() { /** * Addressable events (kind 30000-39999, NIP-01 §"Kinds") are replaced - * by `(pubkey, kind, d)`. Uses a real signed [LongTextNoteEvent] because + * by `(pubkey, kind, d)`. Uses a real signed [LongFormContentEvent] because * the SQLite store dispatches on the typed `AddressableEvent` subclass * to extract the d-tag — synthetic plain `Event`s aren't recognised. */ @@ -358,15 +358,15 @@ class Nip01ComplianceTest : RelayClientTest() { fun parameterizedReplaceableEventsKeepNewestPerDTag() = runBlocking { val signer = NostrSignerSync(KeyPair()) - val v1 = signer.sign(LongTextNoteEvent.build("old", "title", dTag = "list-a", createdAt = 100)) - val v2 = signer.sign(LongTextNoteEvent.build("new", "title", dTag = "list-a", createdAt = 200)) - val v3 = signer.sign(LongTextNoteEvent.build("list-b", "title", dTag = "list-b", createdAt = 100)) + val v1 = signer.sign(LongFormContentEvent.build("old", "title", dTag = "list-a", createdAt = 100)) + val v2 = signer.sign(LongFormContentEvent.build("new", "title", dTag = "list-a", createdAt = 200)) + val v3 = signer.sign(LongFormContentEvent.build("list-b", "title", dTag = "list-b", createdAt = 100)) preload(v1, v2, v3) val (events, _) = client.collectUntilEose( defaultRelayUrl, - Filter(kinds = listOf(LongTextNoteEvent.KIND), authors = listOf(signer.pubKey)), + Filter(kinds = listOf(LongFormContentEvent.KIND), authors = listOf(signer.pubKey)), ) assertEquals(2, events.size) diff --git a/geode/src/test/kotlin/com/vitorpamplona/geode/Nip09DeletionTest.kt b/geode/src/test/kotlin/com/vitorpamplona/geode/Nip09DeletionTest.kt index 914da8e385..03336f4779 100644 --- a/geode/src/test/kotlin/com/vitorpamplona/geode/Nip09DeletionTest.kt +++ b/geode/src/test/kotlin/com/vitorpamplona/geode/Nip09DeletionTest.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -123,7 +123,7 @@ class Nip09DeletionTest { runBlocking { val signer = NostrSignerSync(KeyPair()) val note = signer.sign(TextNoteEvent.build("delete me")) - val deletion = signer.sign(DeletionEvent.build(listOf(note), createdAt = note.createdAt + 1)) + val deletion = signer.sign(DeletionRequestEvent.build(listOf(note), createdAt = note.createdAt + 1)) // Publish original, confirm it's stored. assertEquals(true, client.publishAndConfirm(note, setOf(relayUrl))) @@ -139,12 +139,12 @@ class Nip09DeletionTest { runBlocking { val signer = NostrSignerSync(KeyPair()) val note = signer.sign(TextNoteEvent.build("a")) - val deletion = signer.sign(DeletionEvent.build(listOf(note), createdAt = note.createdAt + 1)) + val deletion = signer.sign(DeletionRequestEvent.build(listOf(note), createdAt = note.createdAt + 1)) client.publishAndConfirm(note, setOf(relayUrl)) client.publishAndConfirm(deletion, setOf(relayUrl)) - val results = query(Filter(kinds = listOf(DeletionEvent.KIND), authors = listOf(signer.pubKey))) + val results = query(Filter(kinds = listOf(DeletionRequestEvent.KIND), authors = listOf(signer.pubKey))) assertEquals(1, results.size) assertEquals(deletion.id, results[0].id) } @@ -154,7 +154,7 @@ class Nip09DeletionTest { runBlocking { val signer = NostrSignerSync(KeyPair()) val note = signer.sign(TextNoteEvent.build("once-upon-a-time")) - val deletion = signer.sign(DeletionEvent.build(listOf(note), createdAt = note.createdAt + 1)) + val deletion = signer.sign(DeletionRequestEvent.build(listOf(note), createdAt = note.createdAt + 1)) client.publishAndConfirm(note, setOf(relayUrl)) client.publishAndConfirm(deletion, setOf(relayUrl)) @@ -178,7 +178,7 @@ class Nip09DeletionTest { // kind-5 event itself (it's just an event), but the SQL DELETE // is owner-scoped, so Alice's event survives. val malloryDelete = - mallory.sign(DeletionEvent.build(listOf(aliceNote), createdAt = aliceNote.createdAt + 1)) + mallory.sign(DeletionRequestEvent.build(listOf(aliceNote), createdAt = aliceNote.createdAt + 1)) client.publishAndConfirm(malloryDelete, setOf(relayUrl)) assertEquals( diff --git a/quartz/CLIENT.md b/quartz/CLIENT.md index 99338a8821..0b03c120ab 100644 --- a/quartz/CLIENT.md +++ b/quartz/CLIENT.md @@ -460,9 +460,9 @@ Common event kinds you'll work with: | 0 | NIP-01 | `MetadataEvent` | User metadata (name, picture, about) | | 1 | NIP-01 | `TextNoteEvent` | Text note (short post) | | 3 | NIP-02 | `ContactListEvent` | Contact list / follow list | -| 4 | NIP-04 | `PrivateDmEvent` | Encrypted direct message | +| 4 | NIP-04 | `EncryptedDmEvent` | Encrypted direct message | | 7 | NIP-25 | `ReactionEvent` | Reaction (like, emoji) | -| 30023 | NIP-23 | `LongTextNoteEvent` | Long-form content (articles) | +| 30023 | NIP-23 | `LongFormContentEvent` | Long-form content (articles) | ## Multi-Relay Subscriptions diff --git a/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip01Core/EventSigCheck.kt b/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip01Core/EventSigCheck.kt index 7b53776a8f..3bac4762e7 100644 --- a/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip01Core/EventSigCheck.kt +++ b/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip01Core/EventSigCheck.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.crypto.verifyId import com.vitorpamplona.quartz.nip01Core.crypto.verifySignature import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.utils.EventFactory import org.junit.Assert.assertEquals @@ -139,7 +139,7 @@ class EventSigCheck { @Test fun checkSerializationLargeZap() { val event = - EventFactory.create<LnZapEvent>( + EventFactory.create<ZapReceiptEvent>( id = "e7f09fddf39fc6cb604708b6af7b4d4adbb07b412847ebb004064040fe8c4b1e", pubKey = "79f00d3f5a19ec806189fcab03c1be4ff81d18ee4f653c88fac41fe03570f432", createdAt = 1728921077, diff --git a/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt b/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt index d588554f74..e3e0232e4f 100644 --- a/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt +++ b/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceMethod import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoMethod import kotlinx.coroutines.test.runTest @@ -47,7 +47,7 @@ class LnZapPaymentRequestNip44EventTest { val request = GetBalanceMethod.create() val event = - LnZapPaymentRequestEvent.createRequest( + NwcRequestEvent.createRequest( request = request, walletServicePubkey = walletServicePubkey, signer = clientSigner, @@ -71,7 +71,7 @@ class LnZapPaymentRequestNip44EventTest { val request = GetInfoMethod.create() val event = - LnZapPaymentRequestEvent.createRequest( + NwcRequestEvent.createRequest( request = request, walletServicePubkey = walletServicePubkey, signer = clientSigner, diff --git a/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt b/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt index 367c45a736..232e0f9708 100644 --- a/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt +++ b/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt @@ -92,13 +92,13 @@ class PrivateZapTests { ) val privateZapRequest = - LnZapRequestEvent.create( + ZapRequestEvent.create( zappedEvent = poll, relays = setOf(RelayUrlNormalizer.normalize("wss://relay.damus.io/")), signer = loggedIn, pollOption = 0, message = "", - zapType = LnZapEvent.ZapType.PRIVATE, + zapType = ZapReceiptEvent.ZapType.PRIVATE, toUserPubHex = null, ) @@ -151,7 +151,7 @@ class PrivateZapTests { ) val privateZapRequest = - LnZapRequestEvent.create( + ZapRequestEvent.create( zappedEvent = textNote, relays = setOf( @@ -162,7 +162,7 @@ class PrivateZapTests { signer = loggedIn, pollOption = null, message = "test", - zapType = LnZapEvent.ZapType.PRIVATE, + zapType = ZapReceiptEvent.ZapType.PRIVATE, toUserPubHex = null, ) diff --git a/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip59GiftWraps/GiftWrapEventTest.kt b/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip59GiftWraps/GiftWrapEventTest.kt index 47455faeb5..ebdd83efbc 100644 --- a/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip59GiftWraps/GiftWrapEventTest.kt +++ b/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip59GiftWraps/GiftWrapEventTest.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip17Dm.NIP17Factory import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.utils.Hex import kotlinx.coroutines.test.runTest @@ -65,7 +65,7 @@ class GiftWrapEventTest { val eventsReceiverGets = events.wraps.filter { it.isTaggedUser(receiver.pubKey) } eventsReceiverGets.forEach { val event = it.unwrapThrowing(receiver) - if (event is SealedRumorEvent) { + if (event is SealEvent) { val innerData = event.unsealThrowing(receiver) assertEquals(message, innerData.content) } else { @@ -77,7 +77,7 @@ class GiftWrapEventTest { val eventsSenderGets = events.wraps.filter { it.isTaggedUser(sender.pubKey) } eventsSenderGets.forEach { val event = it.unwrapThrowing(sender) - if (event is SealedRumorEvent) { + if (event is SealEvent) { val innerData = event.unsealThrowing(sender) assertEquals(message, innerData.content) } else { @@ -118,7 +118,7 @@ class GiftWrapEventTest { val eventsReceiverGets = events.wraps.filter { it.isTaggedUser(receiver.pubKey) } eventsReceiverGets.forEach { val event = it.unwrapThrowing(receiver) - if (event is SealedRumorEvent) { + if (event is SealEvent) { val innerData = event.unsealThrowing(receiver) assertEquals(message, innerData.content) } else { @@ -131,7 +131,7 @@ class GiftWrapEventTest { val eventsSenderGets = events.wraps.filter { it.isTaggedUser(sender.pubKey) } eventsSenderGets.forEach { val event = it.unwrapThrowing(sender) - if (event is SealedRumorEvent) { + if (event is SealEvent) { val innerData = event.unsealThrowing(sender) assertEquals(message, innerData.content) } else { @@ -155,7 +155,7 @@ class GiftWrapEventTest { ) // MsgFor the Receiver val encMsgFromSenderToReceiver = - SealedRumorEvent.create( + SealEvent.create( event = senderMessage, encryptTo = receiver.pubKey, signer = sender, @@ -184,7 +184,7 @@ class GiftWrapEventTest { // MsgFor the Sender val encMsgFromSenderToSender = - SealedRumorEvent.create( + SealEvent.create( event = senderMessage, encryptTo = sender.pubKey, signer = sender, @@ -221,10 +221,10 @@ class GiftWrapEventTest { assertNotNull(giftWrapToReceiver) val unwrappedMsgForSenderBySender = giftWrapToSender.unwrapThrowing(sender) - assertEquals(SealedRumorEvent.KIND, unwrappedMsgForSenderBySender.kind) - assertTrue(unwrappedMsgForSenderBySender is SealedRumorEvent) + assertEquals(SealEvent.KIND, unwrappedMsgForSenderBySender.kind) + assertTrue(unwrappedMsgForSenderBySender is SealEvent) - if (unwrappedMsgForSenderBySender is SealedRumorEvent) { + if (unwrappedMsgForSenderBySender is SealEvent) { val unwrappedRumorToSenderBySender = unwrappedMsgForSenderBySender.unsealThrowing(sender) assertEquals("Hi There!", unwrappedRumorToSenderBySender.content) @@ -242,10 +242,10 @@ class GiftWrapEventTest { } val unwrappedMsgForReceiverByReceiver = giftWrapToReceiver.unwrapThrowing(receiver) - assertEquals(SealedRumorEvent.KIND, unwrappedMsgForReceiverByReceiver.kind) - assertTrue(unwrappedMsgForReceiverByReceiver is SealedRumorEvent) + assertEquals(SealEvent.KIND, unwrappedMsgForReceiverByReceiver.kind) + assertTrue(unwrappedMsgForReceiverByReceiver is SealEvent) - if (unwrappedMsgForReceiverByReceiver is SealedRumorEvent) { + if (unwrappedMsgForReceiverByReceiver is SealEvent) { val unwrappedRumorToReceiverByReceiver = unwrappedMsgForReceiverByReceiver.unsealThrowing(receiver) assertEquals("Hi There!", unwrappedRumorToReceiverByReceiver.content) @@ -273,7 +273,7 @@ class GiftWrapEventTest { ) val msgFromSenderToReceiverA = - SealedRumorEvent.create( + SealEvent.create( event = senderMessage, encryptTo = receiverA.pubKey, signer = sender, @@ -302,7 +302,7 @@ class GiftWrapEventTest { assertEquals(giftWrapForReceiverA.recipientPubKey(), receiverA.pubKey) val msgFromSenderToReceiverB = - SealedRumorEvent.create( + SealEvent.create( event = senderMessage, encryptTo = receiverB.pubKey, signer = sender, @@ -331,7 +331,7 @@ class GiftWrapEventTest { assertEquals(giftWrapForReceiverB.recipientPubKey(), receiverB.pubKey) val msgFromSenderToSender = - SealedRumorEvent.create( + SealEvent.create( event = senderMessage, encryptTo = sender.pubKey, signer = sender, @@ -371,9 +371,9 @@ class GiftWrapEventTest { assertNotNull(giftWrapForReceiverB) val unwrappedMsgForSenderBySender = giftWrapToSender.unwrapThrowing(sender) - assertEquals(SealedRumorEvent.KIND, unwrappedMsgForSenderBySender.kind) + assertEquals(SealEvent.KIND, unwrappedMsgForSenderBySender.kind) - if (unwrappedMsgForSenderBySender is SealedRumorEvent) { + if (unwrappedMsgForSenderBySender is SealEvent) { unwrappedMsgForSenderBySender.unsealOrNull(receiverA)?.let { _ -> fail() } @@ -399,9 +399,9 @@ class GiftWrapEventTest { } val unwrappedMsgForReceiverAByReceiverA = giftWrapForReceiverA.unwrapThrowing(receiverA) - assertEquals(SealedRumorEvent.KIND, unwrappedMsgForReceiverAByReceiverA.kind) + assertEquals(SealEvent.KIND, unwrappedMsgForReceiverAByReceiverA.kind) - if (unwrappedMsgForReceiverAByReceiverA is SealedRumorEvent) { + if (unwrappedMsgForReceiverAByReceiverA is SealEvent) { val unwrappedRumorToReceiverAByReceiverA = unwrappedMsgForReceiverAByReceiverA.unsealThrowing(receiverA) assertEquals("Who is going to the party tonight?", unwrappedRumorToReceiverAByReceiverA.content) @@ -426,9 +426,9 @@ class GiftWrapEventTest { } val unwrappedMsgForReceiverBByReceiverB = giftWrapForReceiverB.unwrapThrowing(receiverB) - assertEquals(SealedRumorEvent.KIND, unwrappedMsgForReceiverBByReceiverB.kind) + assertEquals(SealEvent.KIND, unwrappedMsgForReceiverBByReceiverB.kind) - if (unwrappedMsgForReceiverBByReceiverB is SealedRumorEvent) { + if (unwrappedMsgForReceiverBByReceiverB is SealEvent) { unwrappedMsgForReceiverBByReceiverB.unsealOrNull(receiverA)?.let { unwrappedRumorToReceiverBByReceiverA -> fail() } @@ -617,7 +617,7 @@ class GiftWrapEventTest { assertEquals(signer.pubKey, wrap.recipientPubKey()) val event = wrap.unwrapThrowing(signer) - return if (event is SealedRumorEvent) { + return if (event is SealEvent) { event.unsealThrowing(signer) } else { println(event.toJson()) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/DecryptZapRequest.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/DecryptZapRequest.kt index cdd96dc214..1e74fa6b8f 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/DecryptZapRequest.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/DecryptZapRequest.kt @@ -24,12 +24,12 @@ import android.content.Intent import androidx.core.net.toUri import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent class DecryptZapRequest { companion object { fun assemble( - event: LnZapRequestEvent, + event: ZapRequestEvent, loggedInUser: HexKey, packageName: String, ): Intent { diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt index 2da61eba2a..c5d1a6c388 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt @@ -37,7 +37,7 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult import com.vitorpamplona.quartz.nip55AndroidSigner.client.handlers.BackgroundRequestHandler import com.vitorpamplona.quartz.nip55AndroidSigner.client.handlers.ForegroundRequestHandler import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent class NostrSignerExternal( pubKey: HexKey, @@ -163,7 +163,7 @@ class NostrSignerExternal( throw convertExceptions("Could not derive key", result) } - override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent { + override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent { if (!event.isPrivateZap()) throw SignerExceptions.NothingToDecrypt() val result = backgroundQuery.decryptZapEvent(event) ?: foregroundQuery.decryptZapEvent(event) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/BackgroundRequestHandler.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/BackgroundRequestHandler.kt index d6d7cec689..941d9a8c5e 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/BackgroundRequestHandler.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/BackgroundRequestHandler.kt @@ -40,7 +40,7 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.Nip44D import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.Nip44EncryptQuery import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.SignPsbtQuery import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.SignQuery -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent class BackgroundRequestHandler( loggedInUser: HexKey, @@ -81,7 +81,7 @@ class BackgroundRequestHandler( fromPubKey: HexKey, ) = nip44Decrypt.query(ciphertext, fromPubKey) as? SignerResult.RequestAddressed<DecryptionResult> - fun decryptZapEvent(event: LnZapRequestEvent) = decryptZap.query(event) as? SignerResult.RequestAddressed<ZapEventDecryptionResult> + fun decryptZapEvent(event: ZapRequestEvent) = decryptZap.query(event) as? SignerResult.RequestAddressed<ZapEventDecryptionResult> fun deriveKey(nonce: HexKey) = deriveKey.query(nonce) as? SignerResult.RequestAddressed<DerivationResult> diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/ForegroundRequestHandler.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/ForegroundRequestHandler.kt index 78841f3172..0d695c61c0 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/ForegroundRequestHandler.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/ForegroundRequestHandler.kt @@ -39,7 +39,7 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.respon import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip44EncryptResponse import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.SignPsbtResponse import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.SignResponse -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent class ForegroundRequestHandler( val loggedInUser: HexKey, @@ -86,7 +86,7 @@ class ForegroundRequestHandler( parser = Nip44DecryptResponse::parse, ) - suspend fun decryptZapEvent(event: LnZapRequestEvent) = + suspend fun decryptZapEvent(event: ZapRequestEvent) = launcher.launchWaitAndParse( requestIntentBuilder = { DecryptZapRequest.assemble(event, loggedInUser, packageName) }, parser = DecryptZapResponse::parse, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/README.md b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/README.md index cd8bc2c7ac..e1d2491c59 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/README.md +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/README.md @@ -121,7 +121,7 @@ root as a pinned message. Covered by `nip29RelayGroups/PinEventsTest`. | Kind | Buzz class | Incumbent (registered) | |---|---|---| -| 9041 | `moderation.ModerationUnbanEvent` | `nip75ZapGoals.GoalEvent` | +| 9041 | `moderation.ModerationUnbanEvent` | `nip75ZapGoals.ZapGoalEvent` | | 49001 | `media.MediaUploadEvent` | — (Buzz's own `kind.rs` marks 49001 "Not a relay event kind") | | 30078 | `rsReadState` (helpers) | `nip78AppData.AppSpecificDataEvent` (NIP-RS reuses 30078) | diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/moderation/ModerationUnbanEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/moderation/ModerationUnbanEvent.kt index 7823ddea73..ee3dac4e73 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/moderation/ModerationUnbanEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/buzz/moderation/ModerationUnbanEvent.kt @@ -35,8 +35,8 @@ import com.vitorpamplona.quartz.utils.TimeUtils * `buzz-sdk/src/builders.rs::build_moderation_unban`. * * WARNING — KIND COLLISION: `9041` is also - * [com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent]'s kind in Quartz. This class MUST NOT - * be registered in `utils/EventFactory.kt` (the NIP-75 GoalEvent owns 9041 there). Buzz + * [com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent]'s kind in Quartz. This class MUST NOT + * be registered in `utils/EventFactory.kt` (the NIP-75 ZapGoalEvent owns 9041 there). Buzz * relays dispatch moderation commands by connection context, not by the shared `EventFactory` * kind switch, so leaving 9041 unregistered is correct — construct/parse this type explicitly. */ @@ -53,7 +53,7 @@ class ModerationUnbanEvent( fun target() = tags.moderationTarget() companion object { - /** Kind 9041 — COLLIDES with NIP-75 GoalEvent. Do not register in EventFactory. */ + /** Kind 9041 — COLLIDES with NIP-75 ZapGoalEvent. Do not register in EventFactory. */ const val KIND = 9041 fun build( diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/concord/cord05Invites/ConcordDirectInvite.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/concord/cord05Invites/ConcordDirectInvite.kt index 910eaedb68..f4b1277a9e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/concord/cord05Invites/ConcordDirectInvite.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/concord/cord05Invites/ConcordDirectInvite.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent /** @@ -57,7 +57,7 @@ object ConcordDirectInvite { createdAt: Long, ): GiftWrapEvent { val rumor = RumorAssembler.assembleRumor<Event>(senderSigner.pubKey, createdAt, KIND, emptyArray(), json(invite)) - val seal = SealedRumorEvent.create(rumor, recipientPubKey, senderSigner, createdAt = createdAt) + val seal = SealEvent.create(rumor, recipientPubKey, senderSigner, createdAt = createdAt) // Wrap with a random ephemeral key, adding the ["k","3313"] index tag. val wrapSigner = NostrSignerInternal(KeyPair()) @@ -80,7 +80,7 @@ object ConcordDirectInvite { recipientSigner: NostrSigner, ): CommunityInvite? { val seal = wrap.unwrapOrNull(recipientSigner) ?: return null - if (seal !is SealedRumorEvent) return null + if (seal !is SealEvent) return null val rumor = seal.unsealOrNull(recipientSigner) ?: return null if (rumor.kind != KIND) return null return ConcordJson.decodeOrNull<CommunityInvite>(rumor.content) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/CurationCopy.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/CurationCopy.kt index bb6d37a238..2245686765 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/CurationCopy.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/assistant/CurationCopy.kt @@ -37,7 +37,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag import com.vitorpamplona.quartz.nip01Core.tags.kinds.KindTag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.sha256.sha256 @@ -109,7 +109,7 @@ object CurationCopy { fun buildRemoval( copy: AddressableListItemEvent, createdAt: Long = TimeUtils.now(), - ) = eventTemplate<DeletionEvent>(DeletionEvent.KIND, "", createdAt) { + ) = eventTemplate<DeletionRequestEvent>(DeletionRequestEvent.KIND, "", createdAt) { add(ATag.assemble(copy.address(), null)) add(ETag.assemble(copy.id, null, null)) add(KindTag.assemble(AddressableListItemEvent.KIND)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/graperank/GrapeRankCrawler.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/graperank/GrapeRankCrawler.kt index 4cba6e0c9f..628df753e7 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/graperank/GrapeRankCrawler.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/graperank/GrapeRankCrawler.kt @@ -35,7 +35,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.store.IEventStore import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent @@ -951,7 +951,7 @@ class GrapeRankCrawler( // deleter-must-be-author check the caller runs rejects it. val chunkIds = authorChunk.flatMap { idsByAuthor[it].orEmpty() } for (idChunk in chunkIds.chunked(AUTHORS_PER_FILTER)) { - add(Filter(kinds = listOf(DeletionEvent.KIND), authors = authorChunk, tags = mapOf("e" to idChunk))) + add(Filter(kinds = listOf(DeletionRequestEvent.KIND), authors = authorChunk, tags = mapOf("e" to idChunk))) } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/graperank/GrapeRankPublisher.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/graperank/GrapeRankPublisher.kt index 89b90b79a4..bad314d356 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/graperank/GrapeRankPublisher.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/graperank/GrapeRankPublisher.kt @@ -29,8 +29,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.store.IEventStore -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent -import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.users.followerCount import com.vitorpamplona.quartz.nip85TrustedAssertions.users.hops import com.vitorpamplona.quartz.nip85TrustedAssertions.users.rank @@ -45,7 +45,7 @@ import kotlinx.coroutines.coroutineScope import kotlin.coroutines.cancellation.CancellationException /** - * Persists a set of GrapeRank scores as NIP-85 kind:30382 [ContactCardEvent] + * Persists a set of GrapeRank scores as NIP-85 kind:30382 [UserAssertionEvent] * trusted assertions in the local [IEventStore] — the durable, reusable form of a * score run — and pushes that local card set out to the operator's relays on * demand. The store is the source of truth; the two halves are separable: @@ -168,10 +168,10 @@ class GrapeRankPublisher( idleTimeoutMs: Long = 30_000L, publishTimeoutSecs: Long = 15, ): SyncResult { - val cards = store.query<Event>(Filter(kinds = listOf(ContactCardEvent.KIND), authors = listOf(providerPubkey))) - val deletions = store.query<Event>(Filter(kinds = listOf(DeletionEvent.KIND), authors = listOf(providerPubkey))) + val cards = store.query<Event>(Filter(kinds = listOf(UserAssertionEvent.KIND), authors = listOf(providerPubkey))) + val deletions = store.query<Event>(Filter(kinds = listOf(DeletionRequestEvent.KIND), authors = listOf(providerPubkey))) - val filter = Filter(kinds = listOf(ContactCardEvent.KIND, DeletionEvent.KIND), authors = listOf(providerPubkey)) + val filter = Filter(kinds = listOf(UserAssertionEvent.KIND, DeletionRequestEvent.KIND), authors = listOf(providerPubkey)) val groups = NegentropyStoreSync( client = client, @@ -214,10 +214,10 @@ class GrapeRankPublisher( * store. Locally-retracted cards never show up — inserting their kind:5 * removed them — so a target can be re-carded later without fighting a ghost. */ - private suspend fun existingCards(providerPubkey: HexKey): Map<HexKey, ContactCardEvent> = + private suspend fun existingCards(providerPubkey: HexKey): Map<HexKey, UserAssertionEvent> = store - .query<Event>(Filter(kinds = listOf(ContactCardEvent.KIND), authors = listOf(providerPubkey))) - .filterIsInstance<ContactCardEvent>() + .query<Event>(Filter(kinds = listOf(UserAssertionEvent.KIND), authors = listOf(providerPubkey))) + .filterIsInstance<UserAssertionEvent>() .groupBy { it.aboutUser() } .mapNotNull { (target, cards) -> val t = target.ifBlank { return@mapNotNull null } @@ -230,7 +230,7 @@ class GrapeRankPublisher( * produces no new signature; a missing tag reads null and so differs from a * desired non-null value (the one-time migration onto the new tags). */ - private fun cardValues(card: ContactCardEvent): Triple<Int?, Int?, Int?> = Triple(card.rank(), card.followerCount(), card.hops()) + private fun cardValues(card: UserAssertionEvent): Triple<Int?, Int?, Int?> = Triple(card.rank(), card.followerCount(), card.hops()) /** The (rank, followers, hops) triple a [ScoredCard] would write. */ private fun desiredValues(card: ScoredCard): Triple<Int?, Int?, Int?> = Triple(card.rank, card.followers, card.hops) @@ -254,7 +254,7 @@ class GrapeRankPublisher( batch .map { card -> async(Dispatchers.Default) { - ContactCardEvent.create( + UserAssertionEvent.create( targetUser = card.target, signer = signer, createdAt = createdAt, @@ -285,13 +285,13 @@ class GrapeRankPublisher( */ private suspend fun insertRetractions( signer: NostrSigner, - cards: List<ContactCardEvent>, + cards: List<UserAssertionEvent>, createdAt: Long, ): Int { if (cards.isEmpty()) return 0 var retracted = 0 for (chunk in cards.chunked(DELETE_PER_EVENT)) { - val deletion = signer.sign(DeletionEvent.build(chunk, createdAt)) + val deletion = signer.sign(DeletionRequestEvent.build(chunk, createdAt)) if (insertQuietly(deletion)) retracted += chunk.size } return retracted diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/Nip82ReleaseDetector.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/Nip82ReleaseDetector.kt index a8932e8ec0..088ad5934a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/Nip82ReleaseDetector.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/Nip82ReleaseDetector.kt @@ -24,22 +24,24 @@ import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.tags.AppI import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.tags.VersionTag import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.hasTagWithContent +import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent /** * NIP-82 reuses kind 30063 which was already taken by NIP-51's - * `ReleaseArtifactSetEvent`. Both are valid; they coexist on the wire. + * [ReleaseArtifactSetEvent]. Both are valid; they coexist on the wire and the + * same class parses both. * * We disambiguate by inspecting the tag set: a NIP-82 Software Release MUST * carry both `i` (app identifier) and `version` tags. NIP-51 release artifact * sets do not. */ fun Event.isNip82SoftwareRelease() = - kind == SoftwareReleaseEvent.KIND && + kind == ReleaseArtifactSetEvent.KIND && tags.hasTagWithContent(AppIdTag.TAG_NAME) && tags.hasTagWithContent(VersionTag.TAG_NAME) /** - * Reinterprets `this` as a [SoftwareReleaseEvent]. Use [isNip82SoftwareRelease] + * Reinterprets `this` as a [ReleaseArtifactSetEvent]. Use [isNip82SoftwareRelease] * to gate the call. */ -fun Event.asSoftwareRelease() = SoftwareReleaseEvent(id, pubKey, createdAt, tags, content, sig) +fun Event.asSoftwareRelease() = this as? ReleaseArtifactSetEvent ?: ReleaseArtifactSetEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/SoftwareReleaseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/SoftwareReleaseEvent.kt index e48460d7c7..86712d1400 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/SoftwareReleaseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/SoftwareReleaseEvent.kt @@ -20,91 +20,15 @@ */ package com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAssetEvent -import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.tags.AssetTag -import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder -import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle -import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider -import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate -import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag -import com.vitorpamplona.quartz.nip50Search.IndexableFieldVisitor -import com.vitorpamplona.quartz.nip50Search.SearchableEvent -import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent /** - * NIP-82 Software Release (kind 30063). - * - * A parameterizable replaceable event grouping a set of [SoftwareAssetEvent]s - * under a named release version. The `d` tag is `<app-id>@<version>` and the - * event's `created_at` must be the release date. - * - * NOTE: kind 30063 was also used by NIP-51 `ReleaseArtifactSetEvent`. This - * experimental NIP-82 event is intentionally not registered in `EventFactory` - * to avoid colliding with the existing registration. Callers can instantiate - * or wrap [SoftwareReleaseEvent] directly when they know the event is a NIP-82 - * release. + * NIP-82 software releases share kind 30063 with NIP-51 release artifact sets, and + * [ReleaseArtifactSetEvent] now parses both. This used to be a second class for the same + * kind that `EventFactory` never built. */ -@Immutable -class SoftwareReleaseEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array<Array<String>>, - content: String, - sig: HexKey, -) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig), - EventHintProvider, - SearchableEvent { - // content carries the release notes (free-text). NOTE: kind 30063 collides - // with NIP-51 ReleaseArtifactSetEvent, which is what EventFactory builds for - // stored events — so in practice this override is not exercised at runtime. - override fun indexableContent() = content - - // The read path: the same fields indexableContent() joins, handed over without - // building the joined string a scan would throw away. - override fun forEachIndexableField(visitor: IndexableFieldVisitor) { - visitor.visit(content) - } - - override fun eventHints() = tags.mapNotNull(AssetTag::parseAsHint) - - override fun linkedEventIds() = tags.mapNotNull(AssetTag::parseId) - - fun appId() = tags.appId() - - fun version() = tags.version() - - fun channel() = tags.channel() - - fun assets() = tags.assets() - - companion object { - const val KIND = 30063 - - /** NIP-82 requires `d = <app-id>@<version>`. */ - fun buildDTag( - appId: String, - version: String, - ) = "$appId@$version" - - fun build( - appId: String, - version: String, - channel: String, - assets: List<EventHintBundle<SoftwareAssetEvent>>, - releaseNotes: String = "", - createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<SoftwareReleaseEvent>.() -> Unit = {}, - ) = eventTemplate(KIND, releaseNotes, createdAt) { - dTag(buildDTag(appId, version)) - appId(appId) - version(version) - channel(channel) - assets(assets) - initializer() - } - } -} +@Deprecated( + "Kind 30063 is parsed by ReleaseArtifactSetEvent, which also exposes the NIP-82 fields. Use ReleaseArtifactSetEvent.buildSoftwareRelease to build one.", + ReplaceWith("ReleaseArtifactSetEvent", "com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent"), +) +typealias SoftwareReleaseEvent = ReleaseArtifactSetEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/TagArrayBuilderExt.kt index 42d9195f63..25801b8d75 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/TagArrayBuilderExt.kt @@ -27,13 +27,14 @@ import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.tags.Chan import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.tags.VersionTag import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent -fun TagArrayBuilder<SoftwareReleaseEvent>.appId(appId: String) = addUnique(AppIdTag.assemble(appId)) +fun TagArrayBuilder<ReleaseArtifactSetEvent>.appId(appId: String) = addUnique(AppIdTag.assemble(appId)) -fun TagArrayBuilder<SoftwareReleaseEvent>.version(version: String) = addUnique(VersionTag.assemble(version)) +fun TagArrayBuilder<ReleaseArtifactSetEvent>.version(version: String) = addUnique(VersionTag.assemble(version)) -fun TagArrayBuilder<SoftwareReleaseEvent>.channel(channel: String) = addUnique(ChannelTag.assemble(channel)) +fun TagArrayBuilder<ReleaseArtifactSetEvent>.channel(channel: String) = addUnique(ChannelTag.assemble(channel)) -fun TagArrayBuilder<SoftwareReleaseEvent>.asset(asset: EventHintBundle<SoftwareAssetEvent>) = add(AssetTag.assemble(asset)) +fun TagArrayBuilder<ReleaseArtifactSetEvent>.asset(asset: EventHintBundle<SoftwareAssetEvent>) = add(AssetTag.assemble(asset)) -fun TagArrayBuilder<SoftwareReleaseEvent>.assets(assets: List<EventHintBundle<SoftwareAssetEvent>>) = addAll(AssetTag.assemble(assets)) +fun TagArrayBuilder<ReleaseArtifactSetEvent>.assets(assets: List<EventHintBundle<SoftwareAssetEvent>>) = addAll(AssetTag.assemble(assets)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/publications/PublicationSectionRef.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/publications/PublicationSectionRef.kt index c28219fe8b..bb264bc95f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/publications/PublicationSectionRef.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/publications/PublicationSectionRef.kt @@ -28,8 +28,8 @@ import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.core.isValid -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent /** * One entry in a kind-30040 publication's table of contents. @@ -72,8 +72,8 @@ data class PublicationSectionRef( setOf( PublicationContentEvent.KIND, PublicationIndexEvent.KIND, - LongTextNoteEvent.KIND, - WikiNoteEvent.KIND, + LongFormContentEvent.KIND, + WikiArticleEvent.KIND, NipTextEvent.KIND, ) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt index b1ff7a9483..fd07f11def 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt @@ -88,8 +88,8 @@ import com.vitorpamplona.quartz.marmot.mip05PushNotifications.TokenRequestEvent import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip15Marketplace.auction.AuctionEvent import com.vitorpamplona.quartz.nip15Marketplace.bid.BidEvent @@ -99,35 +99,35 @@ import com.vitorpamplona.quartz.nip15Marketplace.product.ProductEvent import com.vitorpamplona.quartz.nip15Marketplace.stall.StallEvent import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip25Reactions.ExternalReactionEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelHideMessageEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMuteUserEvent -import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent +import com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupAdminsEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMembersEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupParticipantsEvent -import com.vitorpamplona.quartz.nip29RelayGroups.metadata.SupportedRolesEvent +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupRolesEvent import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateGroupEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateInviteEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.DeleteEventEvent import com.vitorpamplona.quartz.nip29RelayGroups.moderation.DeleteGroupEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.EditMetadataEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.PutUserEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.RemoveUserEvent -import com.vitorpamplona.quartz.nip29RelayGroups.request.JoinRequestEvent -import com.vitorpamplona.quartz.nip29RelayGroups.request.LeaveRequestEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupCreateInviteEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupDeleteEventEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupEditMetadataEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupPutUserEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupRemoveUserEvent +import com.vitorpamplona.quartz.nip29RelayGroups.request.GroupJoinRequestEvent +import com.vitorpamplona.quartz.nip29RelayGroups.request.GroupLeaveRequestEvent import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiListEvent import com.vitorpamplona.quartz.nip32Labeling.LabelEvent import com.vitorpamplona.quartz.nip34Git.grasp.UserGraspListEvent import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent @@ -141,7 +141,7 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent -import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent +import com.vitorpamplona.quartz.nip38UserStatus.UserStatusEvent import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip43RelayMembers.addMember.RelayAddMemberEvent @@ -151,10 +151,10 @@ import com.vitorpamplona.quartz.nip43RelayMembers.leaveRequest.RelayLeaveRequest import com.vitorpamplona.quartz.nip43RelayMembers.list.RelayMembershipListEvent import com.vitorpamplona.quartz.nip43RelayMembers.removeMember.RelayRemoveMemberEvent import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcInfoEvent import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcNotificationEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.appCurationSet.AppCurationSetEvent @@ -162,26 +162,26 @@ import com.vitorpamplona.quartz.nip51Lists.articleCurationSet.ArticleCurationSet import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent import com.vitorpamplona.quartz.nip51Lists.gitAuthorList.GitAuthorListEvent import com.vitorpamplona.quartz.nip51Lists.gitRepositoryList.GitRepositoryListEvent import com.vitorpamplona.quartz.nip51Lists.goodWikiAuthorList.GoodWikiAuthorListEvent import com.vitorpamplona.quartz.nip51Lists.goodWikiRelayList.GoodWikiRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent import com.vitorpamplona.quartz.nip51Lists.kindMuteSet.KindMuteSetEvent -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent import com.vitorpamplona.quartz.nip51Lists.mediaFollowList.MediaFollowListEvent import com.vitorpamplona.quartz.nip51Lists.mediaStarterPack.MediaStarterPackEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.pictureCurationSet.PictureCurationSetEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent @@ -189,7 +189,7 @@ import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent import com.vitorpamplona.quartz.nip51Lists.videoCurationSet.VideoCurationSetEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import com.vitorpamplona.quartz.nip52Calendar.rsvp.CalendarRSVPEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent @@ -199,19 +199,19 @@ import com.vitorpamplona.quartz.nip53LiveActivities.nestsServers.NestsServersEve import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent import com.vitorpamplona.quartz.nip53LiveActivities.raid.LiveActivitiesRaidEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip54Wiki.WikiMergeAcceptanceEvent import com.vitorpamplona.quartz.nip54Wiki.WikiMergeRequestEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip54Wiki.WikiRedirectEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent @@ -238,15 +238,15 @@ import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.RelayDiscoveryEvent import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.RelayMonitorEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip69P2pOrderEvents.P2POrderEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent import com.vitorpamplona.quartz.nip72ModCommunities.rules.CommunityRulesEvent -import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent +import com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent import com.vitorpamplona.quartz.nip78AppData.AppDataEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent @@ -255,7 +255,7 @@ import com.vitorpamplona.quartz.nip85TrustedAssertions.addressables.AddressableA import com.vitorpamplona.quartz.nip85TrustedAssertions.events.EventAssertionEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.externalIds.ExternalIdAssertionEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent -import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent import com.vitorpamplona.quartz.nip87Ecash.fedimint.FedimintEvent import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent @@ -263,46 +263,46 @@ import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.contentSearch.NIP90ContentSearchRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.contentSearch.NIP90ContentSearchResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.eventCount.NIP90EventCountRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.eventCount.NIP90EventCountResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.eventPowDelegation.NIP90EventPowDelegationRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.eventPowDelegation.NIP90EventPowDelegationResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.eventPublishSchedule.NIP90EventPublishScheduleRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.eventPublishSchedule.NIP90EventPublishScheduleResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.eventTimestamping.NIP90EventTimestampingRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.eventTimestamping.NIP90EventTimestampingResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.imageGeneration.NIP90ImageGenerationRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.imageGeneration.NIP90ImageGenerationResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.imageToVideo.NIP90ImageToVideoRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.imageToVideo.NIP90ImageToVideoResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.malwareScanning.NIP90MalwareScanRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.malwareScanning.NIP90MalwareScanResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.opReturn.NIP90OpReturnRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.opReturn.NIP90OpReturnResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.peopleSearch.NIP90PeopleSearchRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.peopleSearch.NIP90PeopleSearchResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent -import com.vitorpamplona.quartz.nip90Dvms.summarization.NIP90SummarizationRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.summarization.NIP90SummarizationResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.textExtraction.NIP90TextExtractionRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.textExtraction.NIP90TextExtractionResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.textGeneration.NIP90TextGenerationRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.textGeneration.NIP90TextGenerationResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.textToSpeech.NIP90TextToSpeechRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.textToSpeech.NIP90TextToSpeechResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.translation.NIP90TranslationRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.translation.NIP90TranslationResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryRequest.NIP90UserDiscoveryRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryResponse.NIP90UserDiscoveryResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.videoConversion.NIP90VideoConversionRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.videoConversion.NIP90VideoConversionResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.videoTranslation.NIP90VideoTranslationRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.videoTranslation.NIP90VideoTranslationResponseEvent -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.DvmContentDiscoveryRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.contentSearch.DvmContentSearchRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.contentSearch.DvmContentSearchResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.eventCount.DvmEventCountRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.eventCount.DvmEventCountResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.eventPowDelegation.DvmEventPowDelegationRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.eventPowDelegation.DvmEventPowDelegationResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.eventPublishSchedule.DvmEventPublishScheduleRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.eventPublishSchedule.DvmEventPublishScheduleResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.eventTimestamping.DvmEventTimestampingRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.eventTimestamping.DvmEventTimestampingResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.imageGeneration.DvmImageGenerationRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.imageGeneration.DvmImageGenerationResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.imageToVideo.DvmImageToVideoRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.imageToVideo.DvmImageToVideoResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.malwareScanning.DvmMalwareScanRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.malwareScanning.DvmMalwareScanResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.opReturn.DvmOpReturnRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.opReturn.DvmOpReturnResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.peopleSearch.DvmPeopleSearchRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.peopleSearch.DvmPeopleSearchResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.status.DvmStatusEvent +import com.vitorpamplona.quartz.nip90Dvms.summarization.DvmSummarizationRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.summarization.DvmSummarizationResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.textExtraction.DvmTextExtractionRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.textExtraction.DvmTextExtractionResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.textGeneration.DvmTextGenerationRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.textGeneration.DvmTextGenerationResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.textToSpeech.DvmTextToSpeechRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.textToSpeech.DvmTextToSpeechResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.translation.DvmTranslationRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.translation.DvmTranslationResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryRequest.DvmUserDiscoveryRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryResponse.DvmUserDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.videoConversion.DvmVideoConversionRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.videoConversion.DvmVideoConversionResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.videoTranslation.DvmVideoTranslationRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.videoTranslation.DvmVideoTranslationResponseEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent @@ -387,12 +387,12 @@ object KindNames { BookmarkListEvent.KIND to KindName("Bookmark List", "51"), OldBookmarkListEvent.KIND to KindName("Old Bookmark List", "51"), CalendarDateSlotEvent.KIND to KindName("Day Appointment", "52"), - CalendarEvent.KIND to KindName("Calendar", "52"), + CalendarCollectionEvent.KIND to KindName("Calendar", "52"), CalendarTimeSlotEvent.KIND to KindName("Appointment", "52"), CalendarRSVPEvent.KIND to KindName("Appt RSVP", "52"), ChessGameEvent.KIND to KindName("Chess Games", "64"), JesterEvent.KIND to KindName("Chess Auth", "64"), - RelayFeedsListEvent.KIND to KindName("Favorite Relays", "51"), + FavoriteRelayListEvent.KIND to KindName("Favorite Relays", "51"), LiveChessGameChallengeEvent.KIND to KindName("Chess Challenges", "64"), LiveChessGameAcceptEvent.KIND to KindName("Chess Game Accept", "64"), LiveChessMoveEvent.KIND to KindName("Chess Move", "64"), @@ -400,13 +400,13 @@ object KindNames { LiveChessDrawOfferEvent.KIND to KindName("Chess Draw Offer", "64"), ChannelCreateEvent.KIND to KindName("Channel Definition", "28"), ChannelHideMessageEvent.KIND to KindName("Channel Hide Msg", "28"), - ChannelListEvent.KIND to KindName("Channel List", "28"), + PublicChatListEvent.KIND to KindName("Channel List", "28"), ChannelMessageEvent.KIND to KindName("Channel Message", "28"), ChannelMetadataEvent.KIND to KindName("Channel Metadata", "28"), ChannelMuteUserEvent.KIND to KindName("Channel Mute User", "28"), ChatMessageEncryptedFileHeaderEvent.KIND to KindName("DM File", "17"), ChatMessageEvent.KIND to KindName("DM Message", "17"), - ChatMessageRelayListEvent.KIND to KindName("DM Relays", "17"), + DmRelayListEvent.KIND to KindName("DM Relays", "17"), ClassifiedsEvent.KIND to KindName("Classifieds", "99"), CommentEvent.KIND to KindName("Comments", "22"), GeocacheListingEvent.KIND to KindName("Geocache", "CC"), @@ -417,19 +417,19 @@ object KindNames { CommunityListEvent.KIND to KindName("Community List", "72"), CommunityPostApprovalEvent.KIND to KindName("Community Post", "72"), ContactListEvent.KIND to KindName("Follow List", "02"), - DeletionEvent.KIND to KindName("Deletions", "09"), + DeletionRequestEvent.KIND to KindName("Deletions", "09"), DraftWrapEvent.KIND to KindName("Drafts", "37"), EmojiPackEvent.KIND to KindName("Emoji Packs", "30"), - EmojiPackSelectionEvent.KIND to KindName("Emoji Pack List", "30"), + EmojiListEvent.KIND to KindName("Emoji Pack List", "30"), EphemeralChatEvent.KIND to KindName("Ephemeral Chat", null), EphemeralChatListEvent.KIND to KindName("Ephemeral Chatrooms", null), - FileHeaderEvent.KIND to KindName("File Headers", "94"), + FileMetadataEvent.KIND to KindName("File Headers", "94"), ProfileGalleryEntryEvent.KIND to KindName("Profile Gallery", null), FileServersEvent.KIND to KindName("File Servers", "96"), FileStorageEvent.KIND to KindName("Blob Data", null), FileStorageHeaderEvent.KIND to KindName("Blob Headers", null), FhirResourceEvent.KIND to KindName("Medical Data", null), - FollowListEvent.KIND to KindName("Follow Packs", "51"), + StarterPackEvent.KIND to KindName("Follow Packs", "51"), GenericRepostEvent.KIND to KindName("Reposts (16)", "18"), GeohashListEvent.KIND to KindName("Geohash Follows", "51"), GiftWrapEvent.KIND to KindName("GiftWraps", "59"), @@ -438,23 +438,23 @@ object KindNames { GitPatchEvent.KIND to KindName("Git Patch", "34"), GitRepositoryEvent.KIND to KindName("Git Repo", "34"), GitReplyEvent.KIND to KindName("Git Reply", "34"), - GoalEvent.KIND to KindName("Zap Goals", "75"), - HashtagListEvent.KIND to KindName("Hashtag Follows", "51"), + ZapGoalEvent.KIND to KindName("Zap Goals", "75"), + InterestListEvent.KIND to KindName("Hashtag Follows", "51"), HighlightEvent.KIND to KindName("Highlights", "84"), HTTPAuthorizationEvent.KIND to KindName("Http Auth", "98"), IndexerRelayListEvent.KIND to KindName("Index Relay List", "51"), InteractiveStoryPrologueEvent.KIND to KindName("Adventure Prologue", null), InteractiveStorySceneEvent.KIND to KindName("Adventure Scene", null), InteractiveStoryReadingStateEvent.KIND to KindName("Adventure Reading", null), - LabeledBookmarkListEvent.KIND to KindName("Named Bookmarks", "51"), + BookmarkSetEvent.KIND to KindName("Named Bookmarks", "51"), LiveActivitiesChatMessageEvent.KIND to KindName("Live Chats", "53"), LiveActivitiesEvent.KIND to KindName("Live Streams", "53"), - LnZapEvent.KIND to KindName("Zaps", "57"), - LnZapPaymentRequestEvent.KIND to KindName("NWC Request", "47"), - LnZapPaymentResponseEvent.KIND to KindName("NWC Response", "47"), + ZapReceiptEvent.KIND to KindName("Zaps", "57"), + NwcRequestEvent.KIND to KindName("NWC Request", "47"), + NwcResponseEvent.KIND to KindName("NWC Response", "47"), LnZapPrivateEvent.KIND to KindName("Private Zaps", "57"), - LnZapRequestEvent.KIND to KindName("Zap Req", "57"), - LongTextNoteEvent.KIND to KindName("Blogs", "23"), + ZapRequestEvent.KIND to KindName("Zap Req", "57"), + LongFormContentEvent.KIND to KindName("Blogs", "23"), MeetingRoomEvent.KIND to KindName("Meeting Room", "53"), MeetingRoomPresenceEvent.KIND to KindName("Room Presence", "53"), MeetingSpaceEvent.KIND to KindName("Meeting Space", "53"), @@ -463,14 +463,14 @@ object KindNames { NNSEvent.KIND to KindName("NNS", null), NipTextEvent.KIND to KindName("NIP", null), NostrConnectEvent.KIND to KindName("Nostr Connect", "46"), - NIP90StatusEvent.KIND to KindName("DVM Status", "90"), - NIP90ContentDiscoveryRequestEvent.KIND to KindName("DVM Content Req", "90"), - NIP90ContentDiscoveryResponseEvent.KIND to KindName("DVM Content Resp", "90"), - NIP90UserDiscoveryRequestEvent.KIND to KindName("DVM User Req", "90"), - NIP90UserDiscoveryResponseEvent.KIND to KindName("DVM User Resp", "90"), + DvmStatusEvent.KIND to KindName("DVM Status", "90"), + DvmContentDiscoveryRequestEvent.KIND to KindName("DVM Content Req", "90"), + DvmContentDiscoveryResponseEvent.KIND to KindName("DVM Content Resp", "90"), + DvmUserDiscoveryRequestEvent.KIND to KindName("DVM User Req", "90"), + DvmUserDiscoveryResponseEvent.KIND to KindName("DVM User Resp", "90"), OtsEvent.KIND to KindName("OTS", "03"), PaymentTargetsEvent.KIND to KindName("PayTo", null), - PeopleListEvent.KIND to KindName("People Lists", "51"), + FollowSetEvent.KIND to KindName("People Lists", "51"), ProfileBadgesEvent.KIND to KindName("Profile Badges", "58"), PictureEvent.KIND to KindName("Pictures", "68"), WorkoutRecordEvent.KIND to KindName("Workouts", null), @@ -478,12 +478,12 @@ object KindNames { ZapPollEvent.KIND to KindName("Zap Poll", null), PollEvent.KIND to KindName("Poll", "88"), PollResponseEvent.KIND to KindName("Poll Response", "88"), - PrivateDmEvent.KIND to KindName("NIP-04 DMs", "04"), + EncryptedDmEvent.KIND to KindName("NIP-04 DMs", "04"), PrivateOutboxRelayListEvent.KIND to KindName("Private Relays", "37"), ProxyRelayListEvent.KIND to KindName("Proxy Relays", "51"), PublicMessageEvent.KIND to KindName("Public Message", "A4"), ReactionEvent.KIND to KindName("Reactions", "25"), - ContactCardEvent.KIND to KindName("Contact Card", "85"), + UserAssertionEvent.KIND to KindName("Contact Card", "85"), RelayAuthEvent.KIND to KindName("Relay Auth", "42"), RelayDiscoveryEvent.KIND to KindName("Relay Discovery", "66"), RelayMonitorEvent.KIND to KindName("Relay Monitor Announcement", "66"), @@ -491,24 +491,24 @@ object KindNames { ReportEvent.KIND to KindName("Reports", "56"), RepostEvent.KIND to KindName("Reposts", "18"), RequestToVanishEvent.KIND to KindName("User Delete", "62"), - SealedRumorEvent.KIND to KindName("Seals", "59"), + SealEvent.KIND to KindName("Seals", "59"), SearchRelayListEvent.KIND to KindName("Search Relays", "50"), - StatusEvent.KIND to KindName("User Status", "38"), + UserStatusEvent.KIND to KindName("User Status", "38"), TextNoteEvent.KIND to KindName("Notes", "10"), TextNoteModificationEvent.KIND to KindName("Edits", null), TorrentEvent.KIND to KindName("Torrents", "35"), TorrentCommentEvent.KIND to KindName("Torrent Comments", "35"), TrustedRelayListEvent.KIND to KindName("Trusted Relays", "51"), TrustProviderListEvent.KIND to KindName("Trusted Providers", "85"), - VideoHorizontalEvent.KIND to KindName("Video (Repl)", "71"), - VideoVerticalEvent.KIND to KindName("Shorts (Repl)", "71"), + AddressableNormalVideoEvent.KIND to KindName("Video (Repl)", "71"), + AddressableShortVideoEvent.KIND to KindName("Shorts (Repl)", "71"), VideoNormalEvent.KIND to KindName("Video", "71"), VideoShortEvent.KIND to KindName("Shorts", "71"), VoiceEvent.KIND to KindName("Voice Msg", "A0"), VoiceReplyEvent.KIND to KindName("Voice Reply", "A0"), WakeUpEvent.KIND to KindName("WakeUp", null), WebBookmarkEvent.KIND to KindName("Web Bookmark", "B0"), - WikiNoteEvent.KIND to KindName("Wiki", "54"), + WikiArticleEvent.KIND to KindName("Wiki", "54"), WikiMergeRequestEvent.KIND to KindName("Wiki Merge Request", "54"), WikiMergeAcceptanceEvent.KIND to KindName("Wiki Merge Accepted", null), WikiRedirectEvent.KIND to KindName("Wiki Redirect", "54"), @@ -544,41 +544,41 @@ object KindNames { LabelEvent.KIND to KindName("Label", "32"), SoftwareAssetEvent.KIND to KindName("Software Asset", "82"), AdminCommandEvent.KIND to KindName("Nests Admin Command", null), - NIP90TextExtractionRequestEvent.KIND to KindName("DVM Text Extraction Req", "90"), - NIP90SummarizationRequestEvent.KIND to KindName("DVM Summarization Req", "90"), - NIP90TranslationRequestEvent.KIND to KindName("DVM Translation Req", "90"), - NIP90TextGenerationRequestEvent.KIND to KindName("DVM Text Generation Req", "90"), - NIP90ImageGenerationRequestEvent.KIND to KindName("DVM Image Generation Req", "90"), + DvmTextExtractionRequestEvent.KIND to KindName("DVM Text Extraction Req", "90"), + DvmSummarizationRequestEvent.KIND to KindName("DVM Summarization Req", "90"), + DvmTranslationRequestEvent.KIND to KindName("DVM Translation Req", "90"), + DvmTextGenerationRequestEvent.KIND to KindName("DVM Text Generation Req", "90"), + DvmImageGenerationRequestEvent.KIND to KindName("DVM Image Generation Req", "90"), NappletSnapshotEvent.KIND to KindName("Napplet Snapshot", "5D"), - NIP90VideoConversionRequestEvent.KIND to KindName("DVM Video Conversion Req", "90"), - NIP90VideoTranslationRequestEvent.KIND to KindName("DVM Video Translation Req", "90"), - NIP90ImageToVideoRequestEvent.KIND to KindName("DVM Image To Video Req", "90"), - NIP90TextToSpeechRequestEvent.KIND to KindName("DVM Text To Speech Req", "90"), - NIP90ContentSearchRequestEvent.KIND to KindName("DVM Content Search Req", "90"), - NIP90PeopleSearchRequestEvent.KIND to KindName("DVM People Search Req", "90"), - NIP90EventCountRequestEvent.KIND to KindName("DVM Event Count Req", "90"), - NIP90MalwareScanRequestEvent.KIND to KindName("DVM Malware Scan Req", "90"), - NIP90EventTimestampingRequestEvent.KIND to KindName("DVM Timestamping Req", "90"), - NIP90OpReturnRequestEvent.KIND to KindName("DVM OpReturn Req", "90"), - NIP90EventPublishScheduleRequestEvent.KIND to KindName("DVM Publish Schedule Req", "90"), - NIP90EventPowDelegationRequestEvent.KIND to KindName("DVM PoW Delegation Req", "90"), - NIP90TextExtractionResponseEvent.KIND to KindName("DVM Text Extraction Resp", "90"), - NIP90SummarizationResponseEvent.KIND to KindName("DVM Summarization Resp", "90"), - NIP90TranslationResponseEvent.KIND to KindName("DVM Translation Resp", "90"), - NIP90TextGenerationResponseEvent.KIND to KindName("DVM Text Generation Resp", "90"), - NIP90ImageGenerationResponseEvent.KIND to KindName("DVM Image Generation Resp", "90"), - NIP90VideoConversionResponseEvent.KIND to KindName("DVM Video Conversion Resp", "90"), - NIP90VideoTranslationResponseEvent.KIND to KindName("DVM Video Translation Resp", "90"), - NIP90ImageToVideoResponseEvent.KIND to KindName("DVM Image To Video Resp", "90"), - NIP90TextToSpeechResponseEvent.KIND to KindName("DVM Text To Speech Resp", "90"), - NIP90ContentSearchResponseEvent.KIND to KindName("DVM Content Search Resp", "90"), - NIP90PeopleSearchResponseEvent.KIND to KindName("DVM People Search Resp", "90"), - NIP90EventCountResponseEvent.KIND to KindName("DVM Event Count Resp", "90"), - NIP90MalwareScanResponseEvent.KIND to KindName("DVM Malware Scan Resp", "90"), - NIP90EventTimestampingResponseEvent.KIND to KindName("DVM Timestamping Resp", "90"), - NIP90OpReturnResponseEvent.KIND to KindName("DVM OpReturn Resp", "90"), - NIP90EventPublishScheduleResponseEvent.KIND to KindName("DVM Publish Schedule Resp", "90"), - NIP90EventPowDelegationResponseEvent.KIND to KindName("DVM PoW Delegation Resp", "90"), + DvmVideoConversionRequestEvent.KIND to KindName("DVM Video Conversion Req", "90"), + DvmVideoTranslationRequestEvent.KIND to KindName("DVM Video Translation Req", "90"), + DvmImageToVideoRequestEvent.KIND to KindName("DVM Image To Video Req", "90"), + DvmTextToSpeechRequestEvent.KIND to KindName("DVM Text To Speech Req", "90"), + DvmContentSearchRequestEvent.KIND to KindName("DVM Content Search Req", "90"), + DvmPeopleSearchRequestEvent.KIND to KindName("DVM People Search Req", "90"), + DvmEventCountRequestEvent.KIND to KindName("DVM Event Count Req", "90"), + DvmMalwareScanRequestEvent.KIND to KindName("DVM Malware Scan Req", "90"), + DvmEventTimestampingRequestEvent.KIND to KindName("DVM Timestamping Req", "90"), + DvmOpReturnRequestEvent.KIND to KindName("DVM OpReturn Req", "90"), + DvmEventPublishScheduleRequestEvent.KIND to KindName("DVM Publish Schedule Req", "90"), + DvmEventPowDelegationRequestEvent.KIND to KindName("DVM PoW Delegation Req", "90"), + DvmTextExtractionResponseEvent.KIND to KindName("DVM Text Extraction Resp", "90"), + DvmSummarizationResponseEvent.KIND to KindName("DVM Summarization Resp", "90"), + DvmTranslationResponseEvent.KIND to KindName("DVM Translation Resp", "90"), + DvmTextGenerationResponseEvent.KIND to KindName("DVM Text Generation Resp", "90"), + DvmImageGenerationResponseEvent.KIND to KindName("DVM Image Generation Resp", "90"), + DvmVideoConversionResponseEvent.KIND to KindName("DVM Video Conversion Resp", "90"), + DvmVideoTranslationResponseEvent.KIND to KindName("DVM Video Translation Resp", "90"), + DvmImageToVideoResponseEvent.KIND to KindName("DVM Image To Video Resp", "90"), + DvmTextToSpeechResponseEvent.KIND to KindName("DVM Text To Speech Resp", "90"), + DvmContentSearchResponseEvent.KIND to KindName("DVM Content Search Resp", "90"), + DvmPeopleSearchResponseEvent.KIND to KindName("DVM People Search Resp", "90"), + DvmEventCountResponseEvent.KIND to KindName("DVM Event Count Resp", "90"), + DvmMalwareScanResponseEvent.KIND to KindName("DVM Malware Scan Resp", "90"), + DvmEventTimestampingResponseEvent.KIND to KindName("DVM Timestamping Resp", "90"), + DvmOpReturnResponseEvent.KIND to KindName("DVM OpReturn Resp", "90"), + DvmEventPublishScheduleResponseEvent.KIND to KindName("DVM Publish Schedule Resp", "90"), + DvmEventPowDelegationResponseEvent.KIND to KindName("DVM PoW Delegation Resp", "90"), CashuMintQuoteEvent.KIND to KindName("Cashu Mint Quote", "60"), CashuTokenEvent.KIND to KindName("Cashu Token", "60"), CashuSpendingHistoryEvent.KIND to KindName("Cashu History", "60"), @@ -587,15 +587,15 @@ object KindNames { OnchainZapEvent.KIND to KindName("Onchain Zap", "BC"), Bolt12ZapEvent.KIND to KindName("Bolt12 Zap", "B1"), Bolt12OfferListEvent.KIND to KindName("Bolt12 Offers", "B1"), - PutUserEvent.KIND to KindName("Group Put User", "29"), - RemoveUserEvent.KIND to KindName("Group Remove User", "29"), - EditMetadataEvent.KIND to KindName("Group Edit Metadata", "29"), - DeleteEventEvent.KIND to KindName("Group Delete Event", "29"), + GroupPutUserEvent.KIND to KindName("Group Put User", "29"), + GroupRemoveUserEvent.KIND to KindName("Group Remove User", "29"), + GroupEditMetadataEvent.KIND to KindName("Group Edit Metadata", "29"), + GroupDeleteEventEvent.KIND to KindName("Group Delete Event", "29"), CreateGroupEvent.KIND to KindName("Group Create", "29"), DeleteGroupEvent.KIND to KindName("Group Delete", "29"), - CreateInviteEvent.KIND to KindName("Group Create Invite", "29"), - JoinRequestEvent.KIND to KindName("Group Join Request", "29"), - LeaveRequestEvent.KIND to KindName("Group Leave Request", "29"), + GroupCreateInviteEvent.KIND to KindName("Group Create Invite", "29"), + GroupJoinRequestEvent.KIND to KindName("Group Join Request", "29"), + GroupLeaveRequestEvent.KIND to KindName("Group Leave Request", "29"), NutzapEvent.KIND to KindName("Nutzap", "61"), SimpleGroupListEvent.KIND to KindName("Group List", "51"), ExternalIdentitiesEvent.KIND to KindName("External Identities", "39"), @@ -668,7 +668,7 @@ object KindNames { GroupMetadataEvent.KIND to KindName("Group Metadata", "29"), GroupAdminsEvent.KIND to KindName("Group Admins", "29"), GroupMembersEvent.KIND to KindName("Group Members", "29"), - SupportedRolesEvent.KIND to KindName("Group Roles", "29"), + GroupRolesEvent.KIND to KindName("Group Roles", "29"), GroupParticipantsEvent.KIND to KindName("Group Participants", "29"), MediaStarterPackEvent.KIND to KindName("Media Starter Pack", "51"), ) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/MarmotWelcomeSender.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/MarmotWelcomeSender.kt index 051aaf9742..d83d539c10 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/MarmotWelcomeSender.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/MarmotWelcomeSender.kt @@ -50,7 +50,7 @@ data class WelcomeDelivery( * * Delivery pipeline: * Welcome bytes → base64 → WelcomeEvent (kind:444, unsigned rumor) - * → SealedRumorEvent (kind:13, encrypted with sender's key) + * → SealEvent (kind:13, encrypted with sender's key) * → GiftWrapEvent (kind:1059, encrypted with ephemeral key) */ class MarmotWelcomeSender( diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/RecipientRelayFetcher.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/RecipientRelayFetcher.kt index 846b6c1237..56d268fda5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/RecipientRelayFetcher.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/RecipientRelayFetcher.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchAll import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent /** @@ -100,7 +100,7 @@ object RecipientRelayFetcher { Filter( kinds = listOf( - ChatMessageRelayListEvent.KIND, + DmRelayListEvent.KIND, KeyPackageRelayListEvent.KIND, AdvertisedRelayListEvent.KIND, ), @@ -113,7 +113,7 @@ object RecipientRelayFetcher { idleTimeoutMs = idleTimeoutMs, ) - var dm: ChatMessageRelayListEvent? = null + var dm: DmRelayListEvent? = null var kp: KeyPackageRelayListEvent? = null var nip65: AdvertisedRelayListEvent? = null for (event in events) { @@ -122,7 +122,7 @@ object RecipientRelayFetcher { // route someone else's wrapped welcome to the wrong inbox. if (event.pubKey != pubKey) continue when (event) { - is ChatMessageRelayListEvent -> { + is DmRelayListEvent -> { if (dm == null || event.createdAt > dm.createdAt) dm = event } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip00KeyPackages/KeyPackageRelayListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip00KeyPackages/KeyPackageRelayListEvent.kt index e9eb595b97..3f98a91dec 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip00KeyPackages/KeyPackageRelayListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip00KeyPackages/KeyPackageRelayListEvent.kt @@ -42,7 +42,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils * Replaceable event that advertises which relays hold this user's MLS KeyPackages. * Other clients query this to discover where to fetch KeyPackages for group invitations. * - * Uses the same "relay" tag format as NIP-17's ChatMessageRelayListEvent (kind 10050) + * Uses the same "relay" tag format as NIP-17's DmRelayListEvent (kind 10050) * but serves a different purpose: KeyPackage discovery vs DM inbox routing. */ @Immutable diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip02Welcome/WelcomeGiftWrap.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip02Welcome/WelcomeGiftWrap.kt index 6ee4406313..2f4cc00f48 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip02Welcome/WelcomeGiftWrap.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip02Welcome/WelcomeGiftWrap.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -35,7 +35,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils * * The delivery flow: * WelcomeEvent (unsigned rumor, kind:444) - * → SealedRumorEvent (kind:13, encrypted with sender's key) + * → SealEvent (kind:13, encrypted with sender's key) * → GiftWrapEvent (kind:1059, encrypted with ephemeral key to recipient) * * CRITICAL: The Commit that adds the new member MUST be confirmed by relays @@ -64,7 +64,7 @@ object WelcomeGiftWrap { ): GiftWrapEvent { // Step 1: Build the WelcomeEvent template directly as an unsigned rumor. // Per NIP-59 rumors MUST have an empty sig field, so we skip the outer - // signature entirely and let the SealedRumorEvent carry authorship. + // signature entirely and let the SealEvent carry authorship. val welcomeTemplate = WelcomeEvent.build( welcomeBase64 = welcomeBase64, @@ -82,7 +82,7 @@ object WelcomeGiftWrap { // Step 2: Create a Rumor from the unsigned event and seal it (kind:13) val rumor = Rumor.create(welcomeRumor) val sealedRumor = - SealedRumorEvent.create( + SealEvent.create( rumor = rumor, encryptTo = recipientPubKey, signer = signer, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/cache/projection/EventStoreProjection.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/cache/projection/EventStoreProjection.kt index 58c3ecc6ff..df861928f5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/cache/projection/EventStoreProjection.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/cache/projection/EventStoreProjection.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.store.ObservableEventStore import com.vitorpamplona.quartz.nip01Core.store.ObservableEventStore.StoreChange import com.vitorpamplona.quartz.nip01Core.store.owner -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip40Expiration.isExpirationBefore import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent import com.vitorpamplona.quartz.utils.SortedList @@ -156,7 +156,7 @@ class EventStoreProjection<T : Event>( // NIP-09 / NIP-62 side effects come first — a deletion event // that arrives at the same instant as a matching event still // removes its targets. - if (event is DeletionEvent) { + if (event is DeletionRequestEvent) { if (handleDeletion(event)) changed = true } if (event is RequestToVanishEvent && event.shouldVanishFrom(store.relay)) { @@ -260,7 +260,7 @@ class EventStoreProjection<T : Event>( return changed } - private fun handleDeletion(deletion: DeletionEvent): Boolean { + private fun handleDeletion(deletion: DeletionRequestEvent): Boolean { var changed = false // NIP-09 by id, only if the deletion's author owns the target. diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientNegentropyDeletionSettleExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientNegentropyDeletionSettleExt.kt index 67e770f1de..48ec8f2474 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientNegentropyDeletionSettleExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientNegentropyDeletionSettleExt.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.store.IEventStore import com.vitorpamplona.quartz.nip01Core.store.deletionsCovering -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent /** * Outcome of a [negentropySettleDeletions] run. @@ -129,7 +129,7 @@ suspend fun INostrClient.negentropySettleDeletions( for (chunk in diff.haveIds.chunked(batchSize)) { val ours = store.query<Event>(Filter(ids = chunk)) val relayDeletions = deletionsCovering(ours, relay) { f -> fetchAll(relay, f, idleTimeoutMs) } - for (del in relayDeletions.filterIsInstance<DeletionEvent>()) { + for (del in relayDeletions.filterIsInstance<DeletionRequestEvent>()) { if (del.verify() && appliedDown.add(del.id)) { store.insert(del) resolved++ diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt index 70836a44b9..b87513ce80 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent abstract class NostrSigner( pubKey: HexKey, @@ -67,7 +67,7 @@ abstract class NostrSigner( fromPublicKey: HexKey, ): String - abstract suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent + abstract suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent abstract suspend fun deriveKey(nonce: HexKey): HexKey diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt index b308afbd06..acb512e5c6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import kotlinx.coroutines.CancellationException class NostrSignerInternal( @@ -89,7 +89,7 @@ class NostrSignerInternal( // always ready override fun hasForegroundSupport() = true - override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent { + override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent { if (!event.isPrivateZap()) throw SignerExceptions.NothingToDecrypt() return runWrapErrors { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt index 9ca2935bf5..90566d4e7c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt @@ -31,8 +31,8 @@ import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04 import com.vitorpamplona.quartz.nip44Encryption.Nip44 import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.PrivateZapRequestBuilder +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nipBCOnchainZaps.psbt.Psbt import com.vitorpamplona.quartz.nipBCOnchainZaps.psbt.PsbtSigner @@ -62,7 +62,7 @@ class NostrSignerSync( kind: Int, tags: Array<Array<String>>, ): Boolean = - kind == LnZapRequestEvent.KIND && + kind == ZapRequestEvent.KIND && tags.any { t -> t.size > 1 && t[0] == "anon" && t[1].isBlank() } fun <T : Event> signNormal( @@ -129,7 +129,7 @@ class NostrSignerSync( ) } - fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent = PrivateZapRequestBuilder().decryptZapEvent(event, this) + fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = PrivateZapRequestBuilder().decryptZapEvent(event, this) /** * NIP-BC `sign_psbt`: sign the key-path P2TR inputs of [psbtHex] this key diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/EventStoreDeletionsExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/EventStoreDeletionsExt.kt index 0c5a8203dc..ba070507e1 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/EventStoreDeletionsExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/EventStoreDeletionsExt.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.isAddressable import com.vitorpamplona.quartz.nip01Core.core.isReplaceable import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent /** The addressable/replaceable coordinate of [event] as a NIP-01 `a`-tag value. */ @@ -68,15 +68,15 @@ suspend fun deletionsCovering( val covering = LinkedHashMap<HexKey, Event>() // 1. id-based NIP-09: a kind-5 `e`-tagging an event's id. - query(Filter(kinds = listOf(DeletionEvent.KIND), tags = mapOf("e" to events.map { it.id }))) + query(Filter(kinds = listOf(DeletionRequestEvent.KIND), tags = mapOf("e" to events.map { it.id }))) .forEach { covering[it.id] = it } // 2. address-based NIP-09: a kind-5 `a`-tagging an event's coordinate, cutoff-checked. val byAddress = events.filter { it.kind.isAddressable() || it.kind.isReplaceable() }.groupBy(::addressValue) if (byAddress.isNotEmpty()) { - query(Filter(kinds = listOf(DeletionEvent.KIND), tags = mapOf("a" to byAddress.keys.toList()))) + query(Filter(kinds = listOf(DeletionRequestEvent.KIND), tags = mapOf("a" to byAddress.keys.toList()))) .forEach { del -> - if (del !is DeletionEvent) return@forEach + if (del !is DeletionRequestEvent) return@forEach for (addr in del.deleteAddresses()) { val hit = byAddress[addr.toValue()] ?: continue if (hit.any { it.createdAt <= del.createdAt }) { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionRequestModule.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionRequestModule.kt index 448296a4ae..880e1f4630 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionRequestModule.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionRequestModule.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.isAddressable import com.vitorpamplona.quartz.nip01Core.core.isReplaceable -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent class DeletionRequestModule( val hasher: (db: SQLiteConnection) -> TagNameValueHasher, @@ -83,7 +83,7 @@ class DeletionRequestModule( event: Event, db: SQLiteConnection, ): Int { - if (event !is DeletionEvent) return 0 + if (event !is DeletionRequestEvent) return 0 val idValues = event.deleteEventIds() val addresses = event.deleteAddresses() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt index e57e7c0882..9f267c3d6c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt @@ -38,7 +38,7 @@ import com.vitorpamplona.quartz.nip01Core.store.IEventStore import com.vitorpamplona.quartz.nip01Core.store.IdAndTime import com.vitorpamplona.quartz.nip01Core.store.RawEvent import com.vitorpamplona.quartz.nip01Core.store.RejectionReason -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip40Expiration.isExpired import com.vitorpamplona.quartz.nip50Search.strippingSearchExtensions import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent @@ -349,7 +349,7 @@ class SQLiteEventStore( invalidateAll = true return } - if (event is DeletionEvent && deletedRows > 0) { + if (event is DeletionRequestEvent && deletedRows > 0) { // Only a kind-5 that actually removed rows costs a rebuild. // The common case — a delete broadcast for events this relay // never stored — deletes nothing and records as a plain row, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip04Dm/PrivateDMCache.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip04Dm/PrivateDMCache.kt index 84ad273dfc..18b9c6a075 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip04Dm/PrivateDMCache.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip04Dm/PrivateDMCache.kt @@ -23,14 +23,14 @@ package com.vitorpamplona.quartz.nip04Dm import androidx.collection.LruCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.caches.DecryptCache -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent class PrivateDMCache( signer: NostrSigner, ) { private val decryptionCache = - object : LruCache<PrivateDmEvent, PrivateDMDecryptCache>(10000) { - override fun create(key: PrivateDmEvent): PrivateDMDecryptCache? { + object : LruCache<EncryptedDmEvent, PrivateDMDecryptCache>(10000) { + override fun create(key: EncryptedDmEvent): PrivateDMDecryptCache? { val canDecrypt = key.isIncluded(signer.pubKey) return if (key.content.isNotBlank() && canDecrypt) { PrivateDMDecryptCache(signer) @@ -40,16 +40,16 @@ class PrivateDMCache( } } - fun cachedDM(event: PrivateDmEvent): String? = decryptionCache[event]?.cached() + fun cachedDM(event: EncryptedDmEvent): String? = decryptionCache[event]?.cached() - suspend fun decryptDM(event: PrivateDmEvent) = decryptionCache[event]?.decrypt(event) + suspend fun decryptDM(event: EncryptedDmEvent) = decryptionCache[event]?.decrypt(event) } class PrivateDMDecryptCache( signer: NostrSigner, -) : DecryptCache<PrivateDmEvent, String>(signer) { +) : DecryptCache<EncryptedDmEvent, String>(signer) { override suspend fun decryptAndParse( - event: PrivateDmEvent, + event: EncryptedDmEvent, signer: NostrSigner, ) = event.decryptContent(signer) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip04Dm/messages/EncryptedDmEvent.kt similarity index 92% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip04Dm/messages/EncryptedDmEvent.kt index 9b774e3c49..fc5ec3dd8e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip04Dm/messages/EncryptedDmEvent.kt @@ -42,7 +42,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.persistentSetOf @Immutable -class PrivateDmEvent( +class EncryptedDmEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -124,10 +124,10 @@ class PrivateDmEvent( toUser: PTag, message: String, imetas: List<IMetaTag>? = null, - replyingTo: EventHintBundle<PrivateDmEvent>? = null, + replyingTo: EventHintBundle<EncryptedDmEvent>? = null, createdAt: Long = TimeUtils.now(), signer: NostrSigner, - initializer: TagArrayBuilder<PrivateDmEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<EncryptedDmEvent>.() -> Unit = {}, ) = eventTemplate( kind = KIND, description = @@ -147,9 +147,15 @@ class PrivateDmEvent( to: PTag, message: String, imetas: List<IMetaTag>? = null, - replyingTo: EventHintBundle<PrivateDmEvent>?, + replyingTo: EventHintBundle<EncryptedDmEvent>?, createdAt: Long = TimeUtils.now(), signer: NostrSigner, ) = signer.sign(build(to, message, imetas, replyingTo, createdAt, signer)) } } + +@Deprecated( + "Renamed to EncryptedDmEvent. NIP-04 names kind 4 the encrypted direct message.", + ReplaceWith("EncryptedDmEvent", "com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent"), +) +typealias PrivateDmEvent = EncryptedDmEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip04Dm/messages/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip04Dm/messages/TagArrayBuilderExt.kt index 06d7a5e0b7..8a9d4559ab 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip04Dm/messages/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip04Dm/messages/TagArrayBuilderExt.kt @@ -26,6 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.events.EventReference import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag import com.vitorpamplona.quartz.nip10Notes.tags.toMarkedETag -fun TagArrayBuilder<PrivateDmEvent>.reply(tag: EventReference) = add(MarkedETag.assemble(tag.eventId, tag.relayHint, MarkedETag.MARKER.REPLY, tag.author)) +fun TagArrayBuilder<EncryptedDmEvent>.reply(tag: EventReference) = add(MarkedETag.assemble(tag.eventId, tag.relayHint, MarkedETag.MARKER.REPLY, tag.author)) -fun TagArrayBuilder<PrivateDmEvent>.reply(tag: EventHintBundle<PrivateDmEvent>) = add(tag.toMarkedETag(MarkedETag.MARKER.REPLY).toTagArray()) +fun TagArrayBuilder<EncryptedDmEvent>.reply(tag: EventHintBundle<EncryptedDmEvent>) = add(tag.toMarkedETag(MarkedETag.MARKER.REPLY).toTagArray()) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip09Deletions/DeletionIndex.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip09Deletions/DeletionIndex.kt index a26c716af2..93099829a4 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip09Deletions/DeletionIndex.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip09Deletions/DeletionIndex.kt @@ -45,10 +45,10 @@ class DeletionIndex { // stores a set of id OR atags (kind:pubkey:dtag) by pubkey with the created at of the deletion event. // Anything newer than the date should not be deleted. - private val deletedReferencesBefore = LargeCache<DeletionRequest, DeletionEvent>() + private val deletedReferencesBefore = LargeCache<DeletionRequest, DeletionRequestEvent>() fun add( - event: DeletionEvent, + event: DeletionRequestEvent, wasVerified: Boolean, ): Boolean { var atLeastOne = false @@ -74,7 +74,7 @@ class DeletionIndex { private fun add( ref: String, byPubKey: HexKey, - deletionEvent: DeletionEvent, + deletionEvent: DeletionRequestEvent, wasVerified: Boolean, ): Boolean { val key = DeletionRequest(ref, byPubKey) @@ -89,7 +89,7 @@ class DeletionIndex { return false } - fun hasBeenDeletedBy(event: Event): DeletionEvent? { + fun hasBeenDeletedBy(event: Event): DeletionRequestEvent? { deletedReferencesBefore.get(DeletionRequest(event.id, event.pubKey))?.let { return it } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip09Deletions/DeletionRequestEvent.kt similarity index 90% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip09Deletions/DeletionRequestEvent.kt index ec2f1de70a..b3aad126d8 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip09Deletions/DeletionRequestEvent.kt @@ -44,7 +44,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.pTagIds import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class DeletionEvent( +class DeletionRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -80,7 +80,7 @@ class DeletionEvent( fun build( deleteEvents: List<Event>, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<DeletionEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DeletionRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { deleteEvents.forEach { eTag(ETag(it.id)) @@ -98,7 +98,7 @@ class DeletionEvent( fun buildAddressOnly( deleteEvents: List<Event>, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<DeletionEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DeletionRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { deleteEvents.forEach { if (it is AddressableEvent) { @@ -115,7 +115,7 @@ class DeletionEvent( fun buildForVersionOnly( deleteEvents: List<Event>, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<DeletionEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DeletionRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { deleteEvents.forEach { pTag(PTag(it.pubKey)) @@ -128,3 +128,9 @@ class DeletionEvent( } } } + +@Deprecated( + "Renamed to DeletionRequestEvent. NIP-09 names kind 5 the event deletion request.", + ReplaceWith("DeletionRequestEvent", "com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent"), +) +typealias DeletionEvent = DeletionRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt index b8379fb628..a1f57456d9 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedATags import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.utils.lastNotNullOfOrNull @@ -90,7 +90,7 @@ open class BaseThreadedEvent( val tagAddresses = taggedATags() .filter { aTag -> - aTag.kind != CommunityDefinitionEvent.KIND && (kind != WikiNoteEvent.KIND || aTag.kind != WikiNoteEvent.KIND) && (kind != NipTextEvent.KIND || aTag.kind != NipTextEvent.KIND) + aTag.kind != CommunityDefinitionEvent.KIND && (kind != WikiArticleEvent.KIND || aTag.kind != WikiArticleEvent.KIND) && (kind != NipTextEvent.KIND || aTag.kind != NipTextEvent.KIND) // removes forks from itself. }.map { it.toTag() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip13Pow/signer/PoWNostrSigner.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip13Pow/signer/PoWNostrSigner.kt index de180cdbb6..3e9e00597d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip13Pow/signer/PoWNostrSigner.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip13Pow/signer/PoWNostrSigner.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip13Pow.miner.PoWMiner import com.vitorpamplona.quartz.nip13Pow.tags.PoWTag import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent /** * A [NostrSigner] decorator that mines a NIP-13 proof of work into the unsigned @@ -98,7 +98,7 @@ class PoWNostrSigner( fromPublicKey: HexKey, ): String = signer.nip44Decrypt(ciphertext, fromPublicKey) - override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent = signer.decryptZapEvent(event) + override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = signer.decryptZapEvent(event) override suspend fun deriveKey(nonce: HexKey): HexKey = signer.deriveKey(nonce) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip17Dm/NIP17Factory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip17Dm/NIP17Factory.kt index 5b8284f329..605ce5df89 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip17Dm/NIP17Factory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip17Dm/NIP17Factory.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent @@ -35,7 +35,7 @@ import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nip46RemoteSigner.signer.NostrSignerRemote -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapTemplateConversion import com.vitorpamplona.quartz.utils.mapNotNullAsync @@ -88,7 +88,7 @@ class NIP17Factory { AddressedSeal( recipient = next, seal = - SealedRumorEvent.create( + SealEvent.create( event = event, encryptTo = next, expirationDelta = innerExpDelta, @@ -169,7 +169,7 @@ class NIP17Factory { val build: suspend () -> GiftWrapEvent = { GiftWrapEvent.create( event = - SealedRumorEvent.create( + SealEvent.create( event = event, encryptTo = next, expirationDelta = innerExpDelta, @@ -258,7 +258,7 @@ class NIP17Factory { * it would e-tag the private rumor id onto public relays. */ suspend fun createDeletionNIP17( - template: EventTemplate<DeletionEvent>, + template: EventTemplate<DeletionRequestEvent>, to: List<HexKey>, signer: NostrSigner, ): Result { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip17Dm/settings/DmRelayListEvent.kt similarity index 86% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip17Dm/settings/DmRelayListEvent.kt index da8184c850..c66c51610b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip17Dm/settings/DmRelayListEvent.kt @@ -37,7 +37,7 @@ import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayListDiff import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class ChatMessageRelayListEvent( +class DmRelayListEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -47,7 +47,7 @@ class ChatMessageRelayListEvent( ) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig), DiffableEvent<RelayListDiff> { override fun diffFrom(older: Event): RelayListDiff? { - if (older !is ChatMessageRelayListEvent || older.pubKey != pubKey) return null + if (older !is DmRelayListEvent || older.pubKey != pubKey) return null return RelayListDiff(ListDiff.of(older.relays(), relays(), { it }), ContentChange.NONE) } @@ -71,11 +71,11 @@ class ChatMessageRelayListEvent( .toTypedArray() suspend fun updateRelayList( - earlierVersion: ChatMessageRelayListEvent, + earlierVersion: DmRelayListEvent, relays: List<NormalizedRelayUrl>, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): ChatMessageRelayListEvent { + ): DmRelayListEvent { val tags = earlierVersion.tags .filter(RelayTag::notMatch) @@ -89,12 +89,18 @@ class ChatMessageRelayListEvent( relays: List<NormalizedRelayUrl>, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): ChatMessageRelayListEvent = signer.sign(createdAt, KIND, createTagArray(relays), "") + ): DmRelayListEvent = signer.sign(createdAt, KIND, createTagArray(relays), "") fun create( relays: List<NormalizedRelayUrl>, signer: NostrSignerSync, createdAt: Long = TimeUtils.now(), - ): ChatMessageRelayListEvent = signer.sign(createdAt, KIND, createTagArray(relays), "") + ): DmRelayListEvent = signer.sign(createdAt, KIND, createTagArray(relays), "") } } + +@Deprecated( + "Renamed to DmRelayListEvent. NIP-17 kind 10050 is the relay list to receive DMs.", + ReplaceWith("DmRelayListEvent", "com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent"), +) +typealias ChatMessageRelayListEvent = DmRelayListEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip23LongContent/LongFormContentEvent.kt similarity index 94% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip23LongContent/LongFormContentEvent.kt index 501b0ea929..b4bc6a5cc6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip23LongContent/LongFormContentEvent.kt @@ -56,7 +56,7 @@ import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @Immutable -class LongTextNoteEvent( +class LongFormContentEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -162,7 +162,7 @@ class LongTextNoteEvent( publishedAt: Long? = null, dTag: String = Uuid.random().toString(), createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<LongTextNoteEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<LongFormContentEvent>.() -> Unit = {}, ) = eventTemplate(KIND, description, createdAt) { dTag(dTag) @@ -175,3 +175,9 @@ class LongTextNoteEvent( } } } + +@Deprecated( + "Renamed to LongFormContentEvent. NIP-23 names kind 30023 long-form content.", + ReplaceWith("LongFormContentEvent", "com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent"), +) +typealias LongTextNoteEvent = LongFormContentEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip23LongContent/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip23LongContent/TagArrayBuilderExt.kt index be24865911..174d59c9d0 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip23LongContent/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip23LongContent/TagArrayBuilderExt.kt @@ -26,10 +26,10 @@ import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag -fun TagArrayBuilder<LongTextNoteEvent>.title(title: String) = addUnique(TitleTag.assemble(title)) +fun TagArrayBuilder<LongFormContentEvent>.title(title: String) = addUnique(TitleTag.assemble(title)) -fun TagArrayBuilder<LongTextNoteEvent>.summary(summary: String) = addUnique(SummaryTag.assemble(summary)) +fun TagArrayBuilder<LongFormContentEvent>.summary(summary: String) = addUnique(SummaryTag.assemble(summary)) -fun TagArrayBuilder<LongTextNoteEvent>.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) +fun TagArrayBuilder<LongFormContentEvent>.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) -fun TagArrayBuilder<LongTextNoteEvent>.publishedAt(publishedAt: Long) = addUnique(PublishedAtTag.assemble(publishedAt)) +fun TagArrayBuilder<LongFormContentEvent>.publishedAt(publishedAt: Long) = addUnique(PublishedAtTag.assemble(publishedAt)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/PublicChatListEvent.kt similarity index 89% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/PublicChatListEvent.kt index 27f84bb63c..08cdbbb345 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/PublicChatListEvent.kt @@ -42,7 +42,7 @@ import com.vitorpamplona.quartz.nip51Lists.removeParsing import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class ChannelListEvent( +class PublicChatListEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -53,7 +53,7 @@ class ChannelListEvent( DiffableEvent<ChannelListDiff>, EventHintProvider { override fun diffFrom(older: Event): ChannelListDiff? { - if (older !is ChannelListEvent || older.pubKey != pubKey || older.dTag() != dTag()) return null + if (older !is PublicChatListEvent || older.pubKey != pubKey || older.dTag() != dTag()) return null return ChannelListDiff( ListDiff.of(older.tags.channels(), tags.channels(), { it.eventId }, { a, b -> a.relay == b.relay }), privateItemsChangeFrom(older), @@ -87,7 +87,7 @@ class ChannelListEvent( isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): ChannelListEvent = + ): PublicChatListEvent = if (isPrivate) { create( publicChannels = emptyList(), @@ -105,7 +105,7 @@ class ChannelListEvent( } suspend fun add( - earlierVersion: ChannelListEvent, + earlierVersion: PublicChatListEvent, channel: ChannelTag, isPrivate: Boolean, signer: NostrSigner, @@ -119,12 +119,12 @@ class ChannelListEvent( ) suspend fun add( - earlierVersion: ChannelListEvent, + earlierVersion: PublicChatListEvent, channels: List<ChannelTag>, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): ChannelListEvent = + ): PublicChatListEvent = if (isPrivate) { val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() resign( @@ -143,11 +143,11 @@ class ChannelListEvent( } suspend fun remove( - earlierVersion: ChannelListEvent, + earlierVersion: PublicChatListEvent, channel: ChannelTag, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): ChannelListEvent { + ): PublicChatListEvent { val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() return resign( privateTags = privateTags.removeParsing(ChannelTag::parseId, channel.eventId), @@ -174,14 +174,14 @@ class ChannelListEvent( tags: TagArray, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): ChannelListEvent = signer.sign(createdAt, KIND, tags, content) + ): PublicChatListEvent = signer.sign(createdAt, KIND, tags, content) suspend fun create( publicChannels: List<ChannelTag> = emptyList(), privateChannels: List<ChannelTag> = emptyList(), signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): ChannelListEvent { + ): PublicChatListEvent { val template = build(publicChannels, privateChannels, signer, createdAt) return signer.sign(template) } @@ -191,7 +191,7 @@ class ChannelListEvent( privateChannels: List<ChannelTag> = emptyList(), signer: NostrSignerSync, createdAt: Long = TimeUtils.now(), - ): ChannelListEvent { + ): PublicChatListEvent { val privateTagArray = privateChannels.map { it.toTagArray() }.toTypedArray() val publicTagArray = publicChannels.map { it.toTagArray() }.toTypedArray() return signer.signNip51List(createdAt, KIND, publicTagArray, privateTagArray) @@ -202,8 +202,8 @@ class ChannelListEvent( privateChannels: List<ChannelTag> = emptyList(), signer: NostrSigner, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<ChannelListEvent>.() -> Unit = {}, - ) = eventTemplate<ChannelListEvent>( + initializer: TagArrayBuilder<PublicChatListEvent>.() -> Unit = {}, + ) = eventTemplate<PublicChatListEvent>( kind = KIND, description = PrivateTagsInContent.encryptNip44(privateChannels.map { it.toTagArray() }.toTypedArray(), signer), createdAt = createdAt, @@ -214,3 +214,9 @@ class ChannelListEvent( } } } + +@Deprecated( + "Renamed to PublicChatListEvent. NIP-51 names kind 10005 the public chats list.", + ReplaceWith("PublicChatListEvent", "com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListEvent"), +) +typealias ChannelListEvent = PublicChatListEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/TagArrayBuilderExt.kt index a26e2947f4..fd2acae60b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/TagArrayBuilderExt.kt @@ -26,9 +26,9 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag -fun TagArrayBuilder<ChannelListEvent>.followChat( +fun TagArrayBuilder<PublicChatListEvent>.followChat( eventId: HexKey, relayUrl: NormalizedRelayUrl, ) = addUnique(ETag.assemble(eventId, relayUrl, null)) -fun TagArrayBuilder<ChannelListEvent>.channels(rooms: List<ChannelTag>) = addAll(rooms.map { it.toTagArray() }) +fun TagArrayBuilder<PublicChatListEvent>.channels(rooms: List<ChannelTag>) = addAll(rooms.map { it.toTagArray() }) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/metadata/GroupPinnedEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/metadata/GroupPinnedEvent.kt index a15aa5f589..aa3a653e01 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/metadata/GroupPinnedEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/metadata/GroupPinnedEvent.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils /** * NIP-29 relay-signed list of a group's pinned messages (kind 39005). The relay - * regenerates it from the accepted [com.vitorpamplona.quartz.nip29RelayGroups.moderation.UpdatePinListEvent] + * regenerates it from the accepted [com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupUpdatePinListEvent] * (kind 9010) moderation actions, so this is the read side clients render — the * source of truth for which messages are pinned and in what display order. * diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/metadata/SupportedRolesEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/metadata/GroupRolesEvent.kt similarity index 83% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/metadata/SupportedRolesEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/metadata/GroupRolesEvent.kt index 5efe39736b..9673adce52 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/metadata/SupportedRolesEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/metadata/GroupRolesEvent.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip29RelayGroups.tags.RoleTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class SupportedRolesEvent( +class GroupRolesEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -49,7 +49,7 @@ class SupportedRolesEvent( groupId: String, roles: List<RoleTag>, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<SupportedRolesEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<GroupRolesEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { dTag(groupId) addAll(RoleTag.assemble(roles)) @@ -57,3 +57,9 @@ class SupportedRolesEvent( } } } + +@Deprecated( + "Renamed to GroupRolesEvent. NIP-29 names kind 39003 the group roles. NIP-29 group events carry the Group prefix to keep them apart from NIP-43 relay membership events.", + ReplaceWith("GroupRolesEvent", "com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupRolesEvent"), +) +typealias SupportedRolesEvent = GroupRolesEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/CreateInviteEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/GroupCreateInviteEvent.kt similarity index 82% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/CreateInviteEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/GroupCreateInviteEvent.kt index 6b5ce7f322..ad4fedd9f8 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/CreateInviteEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/GroupCreateInviteEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class CreateInviteEvent( +class GroupCreateInviteEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -47,7 +47,7 @@ class CreateInviteEvent( groupId: String, code: String, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<CreateInviteEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<GroupCreateInviteEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { groupId(groupId) inviteCode(code) @@ -55,3 +55,9 @@ class CreateInviteEvent( } } } + +@Deprecated( + "Renamed to GroupCreateInviteEvent. NIP-29 group events carry the Group prefix to keep them apart from NIP-43 relay membership events.", + ReplaceWith("GroupCreateInviteEvent", "com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupCreateInviteEvent"), +) +typealias CreateInviteEvent = GroupCreateInviteEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/DeleteEventEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/GroupDeleteEventEvent.kt similarity index 84% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/DeleteEventEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/GroupDeleteEventEvent.kt index 25ebd16183..342c1cba71 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/DeleteEventEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/GroupDeleteEventEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class DeleteEventEvent( +class GroupDeleteEventEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -50,7 +50,7 @@ class DeleteEventEvent( eventIds: List<HexKey>, previousEvents: List<String> = emptyList(), createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<DeleteEventEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<GroupDeleteEventEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { groupId(groupId) eventIds.forEach { add(arrayOf("e", it)) } @@ -59,3 +59,9 @@ class DeleteEventEvent( } } } + +@Deprecated( + "Renamed to GroupDeleteEventEvent. NIP-29 group events carry the Group prefix to keep them apart from NIP-43 relay membership events.", + ReplaceWith("GroupDeleteEventEvent", "com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupDeleteEventEvent"), +) +typealias DeleteEventEvent = GroupDeleteEventEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/EditMetadataEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/GroupEditMetadataEvent.kt similarity index 92% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/EditMetadataEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/GroupEditMetadataEvent.kt index 76d0b93ac1..1944ace1a7 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/EditMetadataEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/GroupEditMetadataEvent.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class EditMetadataEvent( +class GroupEditMetadataEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -97,7 +97,7 @@ class EditMetadataEvent( visibility: String? = null, archived: Boolean? = null, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<EditMetadataEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<GroupEditMetadataEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { groupId(groupId) name?.let { add(arrayOf("name", it)) } @@ -116,3 +116,9 @@ class EditMetadataEvent( } } } + +@Deprecated( + "Renamed to GroupEditMetadataEvent. NIP-29 group events carry the Group prefix to keep them apart from NIP-43 relay membership events.", + ReplaceWith("GroupEditMetadataEvent", "com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupEditMetadataEvent"), +) +typealias EditMetadataEvent = GroupEditMetadataEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/PutUserEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/GroupPutUserEvent.kt similarity index 88% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/PutUserEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/GroupPutUserEvent.kt index d2f9382413..6faf482d43 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/PutUserEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/GroupPutUserEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class PutUserEvent( +class GroupPutUserEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -62,7 +62,7 @@ class PutUserEvent( previousEvents: List<String> = emptyList(), buzzRole: String? = null, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<PutUserEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<GroupPutUserEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { groupId(groupId) pubKeysWithRoles.forEach { (pubKey, roles) -> @@ -74,3 +74,9 @@ class PutUserEvent( } } } + +@Deprecated( + "Renamed to GroupPutUserEvent. NIP-29 group events carry the Group prefix to keep them apart from NIP-43 relay membership events.", + ReplaceWith("GroupPutUserEvent", "com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupPutUserEvent"), +) +typealias PutUserEvent = GroupPutUserEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/RemoveUserEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/GroupRemoveUserEvent.kt similarity index 84% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/RemoveUserEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/GroupRemoveUserEvent.kt index 09d7ea35fb..9a690605ee 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/RemoveUserEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/GroupRemoveUserEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class RemoveUserEvent( +class GroupRemoveUserEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -50,7 +50,7 @@ class RemoveUserEvent( pubKeys: List<HexKey>, previousEvents: List<String> = emptyList(), createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<RemoveUserEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<GroupRemoveUserEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { groupId(groupId) pubKeys.forEach { userPubKey(it) } @@ -59,3 +59,9 @@ class RemoveUserEvent( } } } + +@Deprecated( + "Renamed to GroupRemoveUserEvent. NIP-29 group events carry the Group prefix to keep them apart from NIP-43 relay membership events.", + ReplaceWith("GroupRemoveUserEvent", "com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupRemoveUserEvent"), +) +typealias RemoveUserEvent = GroupRemoveUserEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/UpdatePinListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/GroupUpdatePinListEvent.kt similarity index 85% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/UpdatePinListEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/GroupUpdatePinListEvent.kt index 9a53d7e7cc..fe224d5d21 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/UpdatePinListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/moderation/GroupUpdatePinListEvent.kt @@ -35,7 +35,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils * kind-39005 [com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupPinnedEvent]. */ @Immutable -class UpdatePinListEvent( +class GroupUpdatePinListEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -54,7 +54,7 @@ class UpdatePinListEvent( groupId: String, pinnedEventIds: List<HexKey>, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<UpdatePinListEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<GroupUpdatePinListEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { groupId(groupId) pinnedEventIds.forEach { add(arrayOf("e", it)) } @@ -62,3 +62,9 @@ class UpdatePinListEvent( } } } + +@Deprecated( + "Renamed to GroupUpdatePinListEvent. NIP-29 group events carry the Group prefix to keep them apart from NIP-43 relay membership events.", + ReplaceWith("GroupUpdatePinListEvent", "com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupUpdatePinListEvent"), +) +typealias UpdatePinListEvent = GroupUpdatePinListEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/request/JoinRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/request/GroupJoinRequestEvent.kt similarity index 84% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/request/JoinRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/request/GroupJoinRequestEvent.kt index 2db9b25108..86953702f6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/request/JoinRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/request/GroupJoinRequestEvent.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip29RelayGroups.moderation.inviteCode import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class JoinRequestEvent( +class GroupJoinRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -50,7 +50,7 @@ class JoinRequestEvent( reason: String = "", inviteCode: String? = null, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<JoinRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<GroupJoinRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, reason, createdAt) { groupId(groupId) inviteCode?.let { inviteCode(it) } @@ -58,3 +58,9 @@ class JoinRequestEvent( } } } + +@Deprecated( + "Renamed to GroupJoinRequestEvent. NIP-29 group events carry the Group prefix to keep them apart from NIP-43 relay membership events.", + ReplaceWith("GroupJoinRequestEvent", "com.vitorpamplona.quartz.nip29RelayGroups.request.GroupJoinRequestEvent"), +) +typealias JoinRequestEvent = GroupJoinRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/request/LeaveRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/request/GroupLeaveRequestEvent.kt similarity index 82% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/request/LeaveRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/request/GroupLeaveRequestEvent.kt index 429750cc61..574c5cc38d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/request/LeaveRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/request/GroupLeaveRequestEvent.kt @@ -29,7 +29,7 @@ import com.vitorpamplona.quartz.nip29RelayGroups.moderation.groupId import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class LeaveRequestEvent( +class GroupLeaveRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -46,10 +46,16 @@ class LeaveRequestEvent( groupId: String, reason: String = "", createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<LeaveRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<GroupLeaveRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, reason, createdAt) { groupId(groupId) initializer() } } } + +@Deprecated( + "Renamed to GroupLeaveRequestEvent. NIP-29 group events carry the Group prefix to keep them apart from NIP-43 relay membership events.", + ReplaceWith("GroupLeaveRequestEvent", "com.vitorpamplona.quartz.nip29RelayGroups.request.GroupLeaveRequestEvent"), +) +typealias LeaveRequestEvent = GroupLeaveRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiListEvent.kt similarity index 85% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiListEvent.kt index 774f834730..1d2a17c5b6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiListEvent.kt @@ -35,7 +35,7 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class EmojiPackSelectionEvent( +class EmojiListEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -61,20 +61,20 @@ class EmojiPackSelectionEvent( fun createAddressTag(pubKey: HexKey) = Address.assemble(KIND, pubKey, FIXED_D_TAG) fun add( - currentSelection: EmojiPackSelectionEvent, + currentSelection: EmojiListEvent, packToAdd: EventHintBundle<EmojiPackEvent>, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<EmojiPackSelectionEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<EmojiListEvent>.() -> Unit = {}, ) = eventUpdate(currentSelection, createdAt) { pack(packToAdd.toATag()) initializer() } fun remove( - currentSelection: EmojiPackSelectionEvent, + currentSelection: EmojiListEvent, packToRemove: EmojiPackEvent, createdAt: Long = TimeUtils.now(), - updater: TagArrayBuilder<EmojiPackSelectionEvent>.() -> Unit = {}, + updater: TagArrayBuilder<EmojiListEvent>.() -> Unit = {}, ) = eventUpdate(currentSelection, createdAt) { removePack(packToRemove.address()) updater() @@ -83,10 +83,16 @@ class EmojiPackSelectionEvent( fun build( listOfEmojiPacks: List<EventHintBundle<EmojiPackEvent>>, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<EmojiPackSelectionEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<EmojiListEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { packs(listOfEmojiPacks.map { it.toATag() }) initializer() } } } + +@Deprecated( + "Renamed to EmojiListEvent. NIP-51 names kind 10030 the user emoji list.", + ReplaceWith("EmojiListEvent", "com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiListEvent"), +) +typealias EmojiPackSelectionEvent = EmojiListEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/selection/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/selection/TagArrayBuilderExt.kt index c97a08a7fb..455d33dfef 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/selection/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/selection/TagArrayBuilderExt.kt @@ -27,8 +27,8 @@ import com.vitorpamplona.quartz.nip01Core.tags.aTag.aTag import com.vitorpamplona.quartz.nip01Core.tags.aTag.aTags import com.vitorpamplona.quartz.nip01Core.tags.aTag.removeAddress -fun TagArrayBuilder<EmojiPackSelectionEvent>.pack(tag: ATag) = aTag(tag) +fun TagArrayBuilder<EmojiListEvent>.pack(tag: ATag) = aTag(tag) -fun TagArrayBuilder<EmojiPackSelectionEvent>.packs(tags: List<ATag>) = aTags(tags) +fun TagArrayBuilder<EmojiListEvent>.packs(tags: List<ATag>) = aTags(tags) -fun TagArrayBuilder<EmojiPackSelectionEvent>.removePack(address: Address) = removeAddress(address) +fun TagArrayBuilder<EmojiListEvent>.removePack(address: Address) = removeAddress(address) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip38UserStatus/StatusEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip38UserStatus/UserStatusEvent.kt similarity index 90% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip38UserStatus/StatusEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip38UserStatus/UserStatusEvent.kt index 1ddc25cbc9..db4faeae99 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip38UserStatus/StatusEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip38UserStatus/UserStatusEvent.kt @@ -34,7 +34,7 @@ import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class StatusEvent( +class UserStatusEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -70,7 +70,7 @@ class StatusEvent( emojiTags: List<EmojiUrlTag>? = null, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): StatusEvent { + ): UserStatusEvent { val tags = mutableListOf<Array<String>>() tags.add(arrayOf("d", type)) @@ -85,23 +85,29 @@ class StatusEvent( } suspend fun update( - event: StatusEvent, + event: UserStatusEvent, newStatus: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): StatusEvent { + ): UserStatusEvent { val tags = event.tags return signer.sign(createdAt, KIND, tags, newStatus) } suspend fun clear( - event: StatusEvent, + event: UserStatusEvent, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): StatusEvent { + ): UserStatusEvent { val msg = "" val tags = event.tags.filter { it.size > 1 && it[0] == "d" } return signer.sign(createdAt, KIND, tags.toTypedArray(), msg) } } } + +@Deprecated( + "Renamed to UserStatusEvent. NIP-38 names kind 30315 the user status.", + ReplaceWith("UserStatusEvent", "com.vitorpamplona.quartz.nip38UserStatus.UserStatusEvent"), +) +typealias StatusEvent = UserStatusEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt index 5ca77a0410..f58bd0ad69 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt @@ -41,7 +41,7 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestPing import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope @@ -325,7 +325,7 @@ class NostrSignerRemote( throw convertExceptions("Could not get public key", result) } - override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent { + override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent { TODO("Not yet implemented") } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Client.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Client.kt index 4ee11947c6..acee4cec7c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Client.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Client.kt @@ -27,10 +27,10 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcInfoEvent import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcNotificationEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CancelHoldInvoiceMethod import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceMethod import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBudgetMethod @@ -115,7 +115,7 @@ class Nip47Client( suspend fun payInvoice( bolt11: String, amount: Long? = null, - ): LnZapPaymentRequestEvent = + ): NwcRequestEvent = buildRequest( if (amount != null) { PayInvoiceMethod.create(bolt11, amount) @@ -132,17 +132,17 @@ class Nip47Client( pubkey: String, preimage: String? = null, tlvRecords: List<TlvRecord>? = null, - ): LnZapPaymentRequestEvent = buildRequest(PayKeysendMethod.create(amount, pubkey, preimage, tlvRecords)) + ): NwcRequestEvent = buildRequest(PayKeysendMethod.create(amount, pubkey, preimage, tlvRecords)) /** * Builds a get_balance request event. */ - suspend fun getBalance(): LnZapPaymentRequestEvent = buildRequest(GetBalanceMethod.create()) + suspend fun getBalance(): NwcRequestEvent = buildRequest(GetBalanceMethod.create()) /** * Builds a get_info request event. */ - suspend fun getInfo(): LnZapPaymentRequestEvent = buildRequest(GetInfoMethod.create()) + suspend fun getInfo(): NwcRequestEvent = buildRequest(GetInfoMethod.create()) /** * Builds a make_invoice request event. @@ -152,17 +152,17 @@ class Nip47Client( description: String? = null, descriptionHash: String? = null, expiry: Long? = null, - ): LnZapPaymentRequestEvent = buildRequest(MakeInvoiceMethod.create(amount, description, descriptionHash, expiry)) + ): NwcRequestEvent = buildRequest(MakeInvoiceMethod.create(amount, description, descriptionHash, expiry)) /** * Builds a lookup_invoice request event by payment hash. */ - suspend fun lookupInvoiceByHash(paymentHash: String): LnZapPaymentRequestEvent = buildRequest(LookupInvoiceMethod.createByHash(paymentHash)) + suspend fun lookupInvoiceByHash(paymentHash: String): NwcRequestEvent = buildRequest(LookupInvoiceMethod.createByHash(paymentHash)) /** * Builds a lookup_invoice request event by BOLT11 invoice. */ - suspend fun lookupInvoiceByInvoice(invoice: String): LnZapPaymentRequestEvent = buildRequest(LookupInvoiceMethod.createByInvoice(invoice)) + suspend fun lookupInvoiceByInvoice(invoice: String): NwcRequestEvent = buildRequest(LookupInvoiceMethod.createByInvoice(invoice)) /** * Builds a list_transactions request event. @@ -174,17 +174,17 @@ class Nip47Client( offset: Int? = null, unpaid: Boolean? = null, type: String? = null, - ): LnZapPaymentRequestEvent = buildRequest(ListTransactionsMethod.create(from, until, limit, offset, unpaid, type)) + ): NwcRequestEvent = buildRequest(ListTransactionsMethod.create(from, until, limit, offset, unpaid, type)) /** * Builds a get_budget request event. */ - suspend fun getBudget(): LnZapPaymentRequestEvent = buildRequest(GetBudgetMethod.create()) + suspend fun getBudget(): NwcRequestEvent = buildRequest(GetBudgetMethod.create()) /** * Builds a sign_message request event. */ - suspend fun signMessage(message: String): LnZapPaymentRequestEvent = buildRequest(SignMessageMethod.create(message)) + suspend fun signMessage(message: String): NwcRequestEvent = buildRequest(SignMessageMethod.create(message)) /** * Builds a make_hold_invoice request event. @@ -196,24 +196,24 @@ class Nip47Client( descriptionHash: String? = null, expiry: Long? = null, minCltvExpiryDelta: Int? = null, - ): LnZapPaymentRequestEvent = buildRequest(MakeHoldInvoiceMethod.create(amount, paymentHash, description, descriptionHash, expiry, minCltvExpiryDelta)) + ): NwcRequestEvent = buildRequest(MakeHoldInvoiceMethod.create(amount, paymentHash, description, descriptionHash, expiry, minCltvExpiryDelta)) /** * Builds a cancel_hold_invoice request event. */ - suspend fun cancelHoldInvoice(paymentHash: String): LnZapPaymentRequestEvent = buildRequest(CancelHoldInvoiceMethod.create(paymentHash)) + suspend fun cancelHoldInvoice(paymentHash: String): NwcRequestEvent = buildRequest(CancelHoldInvoiceMethod.create(paymentHash)) /** * Builds a settle_hold_invoice request event. */ - suspend fun settleHoldInvoice(preimage: String): LnZapPaymentRequestEvent = buildRequest(SettleHoldInvoiceMethod.create(preimage)) + suspend fun settleHoldInvoice(preimage: String): NwcRequestEvent = buildRequest(SettleHoldInvoiceMethod.create(preimage)) /** * Builds a request event from any [Request] object. * This is the low-level method used by all convenience methods above. */ - suspend fun buildRequest(request: Request): LnZapPaymentRequestEvent = - LnZapPaymentRequestEvent.createRequest( + suspend fun buildRequest(request: Request): NwcRequestEvent = + NwcRequestEvent.createRequest( request = request, walletServicePubkey = walletPubKeyHex, signer = signer, @@ -225,7 +225,7 @@ class Nip47Client( /** * Decrypts and parses a response event from the wallet. */ - suspend fun parseResponse(event: LnZapPaymentResponseEvent): Response = event.decrypt(signer) + suspend fun parseResponse(event: NwcResponseEvent): Response = event.decrypt(signer) /** * Decrypts and parses a notification event from the wallet. @@ -240,7 +240,7 @@ class Nip47Client( */ fun responseFilter(requestEventId: HexKey): Filter = Filter( - kinds = listOf(LnZapPaymentResponseEvent.KIND), + kinds = listOf(NwcResponseEvent.KIND), authors = listOf(walletPubKeyHex), tags = mapOf("e" to listOf(requestEventId)), ) @@ -251,7 +251,7 @@ class Nip47Client( */ fun allResponsesFilter(since: Long? = null): Filter = Filter( - kinds = listOf(LnZapPaymentResponseEvent.KIND), + kinds = listOf(NwcResponseEvent.KIND), authors = listOf(walletPubKeyHex), tags = mapOf("p" to listOf(signer.pubKey)), since = since, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Server.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Server.kt index 739351ca16..da7baebe4c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Server.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Server.kt @@ -23,10 +23,10 @@ package com.vitorpamplona.quartz.nip47WalletConnect import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcInfoEvent import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcNotificationEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceSuccessResponse import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBudgetSuccessResponse import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoSuccessResponse @@ -105,7 +105,7 @@ class Nip47Server( /** * Decrypts and parses an incoming client request. */ - suspend fun parseRequest(event: LnZapPaymentRequestEvent): Request = event.decryptRequest(signer) + suspend fun parseRequest(event: NwcRequestEvent): Request = event.decryptRequest(signer) // --- Response builders --- @@ -114,9 +114,9 @@ class Nip47Server( */ suspend fun buildResponse( response: Response, - requestEvent: LnZapPaymentRequestEvent, - ): LnZapPaymentResponseEvent = - LnZapPaymentResponseEvent.createResponse( + requestEvent: NwcRequestEvent, + ): NwcResponseEvent = + NwcResponseEvent.createResponse( response = response, requestEvent = requestEvent, signer = signer, @@ -127,11 +127,11 @@ class Nip47Server( * Builds an error response for any method. */ suspend fun respondError( - requestEvent: LnZapPaymentRequestEvent, + requestEvent: NwcRequestEvent, code: NwcErrorCode, message: String, resultType: String? = null, - ): LnZapPaymentResponseEvent { + ): NwcResponseEvent { val method = resultType ?: requestEvent.decryptRequest(signer).method ?: NwcMethod.PAY_INVOICE return buildResponse(NwcErrorResponse(method, NwcError(code, message)), requestEvent) } @@ -140,10 +140,10 @@ class Nip47Server( * Builds a pay_invoice success response. */ suspend fun respondPayInvoice( - requestEvent: LnZapPaymentRequestEvent, + requestEvent: NwcRequestEvent, preimage: String? = null, feesPaid: Long? = null, - ): LnZapPaymentResponseEvent = + ): NwcResponseEvent = buildResponse( PayInvoiceSuccessResponse(PayInvoiceSuccessResponse.PayInvoiceResultParams(preimage, feesPaid)), requestEvent, @@ -153,10 +153,10 @@ class Nip47Server( * Builds a pay_keysend success response. */ suspend fun respondPayKeysend( - requestEvent: LnZapPaymentRequestEvent, + requestEvent: NwcRequestEvent, preimage: String? = null, feesPaid: Long? = null, - ): LnZapPaymentResponseEvent = + ): NwcResponseEvent = buildResponse( PayKeysendSuccessResponse(PayKeysendSuccessResponse.PayKeysendResult(preimage, feesPaid)), requestEvent, @@ -166,9 +166,9 @@ class Nip47Server( * Builds a get_balance success response. */ suspend fun respondGetBalance( - requestEvent: LnZapPaymentRequestEvent, + requestEvent: NwcRequestEvent, balance: Long, - ): LnZapPaymentResponseEvent = + ): NwcResponseEvent = buildResponse( GetBalanceSuccessResponse(GetBalanceSuccessResponse.GetBalanceResult(balance)), requestEvent, @@ -178,7 +178,7 @@ class Nip47Server( * Builds a get_info success response. */ suspend fun respondGetInfo( - requestEvent: LnZapPaymentRequestEvent, + requestEvent: NwcRequestEvent, alias: String? = null, color: String? = null, pubkey: String? = null, @@ -188,7 +188,7 @@ class Nip47Server( methods: List<String>? = null, notifications: List<String>? = null, lud16: String? = null, - ): LnZapPaymentResponseEvent = + ): NwcResponseEvent = buildResponse( GetInfoSuccessResponse( GetInfoSuccessResponse.GetInfoResult( @@ -211,26 +211,26 @@ class Nip47Server( * Builds a make_invoice success response. */ suspend fun respondMakeInvoice( - requestEvent: LnZapPaymentRequestEvent, + requestEvent: NwcRequestEvent, transaction: NwcTransaction, - ): LnZapPaymentResponseEvent = buildResponse(MakeInvoiceSuccessResponse(transaction), requestEvent) + ): NwcResponseEvent = buildResponse(MakeInvoiceSuccessResponse(transaction), requestEvent) /** * Builds a lookup_invoice success response. */ suspend fun respondLookupInvoice( - requestEvent: LnZapPaymentRequestEvent, + requestEvent: NwcRequestEvent, transaction: NwcTransaction, - ): LnZapPaymentResponseEvent = buildResponse(LookupInvoiceSuccessResponse(transaction), requestEvent) + ): NwcResponseEvent = buildResponse(LookupInvoiceSuccessResponse(transaction), requestEvent) /** * Builds a list_transactions success response. */ suspend fun respondListTransactions( - requestEvent: LnZapPaymentRequestEvent, + requestEvent: NwcRequestEvent, transactions: List<NwcTransaction>, totalCount: Long? = null, - ): LnZapPaymentResponseEvent = + ): NwcResponseEvent = buildResponse( ListTransactionsSuccessResponse( ListTransactionsSuccessResponse.ListTransactionsResult(transactions, totalCount), @@ -242,12 +242,12 @@ class Nip47Server( * Builds a get_budget success response. */ suspend fun respondGetBudget( - requestEvent: LnZapPaymentRequestEvent, + requestEvent: NwcRequestEvent, usedBudget: Long? = null, totalBudget: Long? = null, renewsAt: Long? = null, renewalPeriod: String? = null, - ): LnZapPaymentResponseEvent = + ): NwcResponseEvent = buildResponse( GetBudgetSuccessResponse( GetBudgetSuccessResponse.GetBudgetResult(usedBudget, totalBudget, renewsAt, renewalPeriod), @@ -259,10 +259,10 @@ class Nip47Server( * Builds a sign_message success response. */ suspend fun respondSignMessage( - requestEvent: LnZapPaymentRequestEvent, + requestEvent: NwcRequestEvent, message: String, signature: String, - ): LnZapPaymentResponseEvent = + ): NwcResponseEvent = buildResponse( SignMessageSuccessResponse(SignMessageSuccessResponse.SignMessageResult(message, signature)), requestEvent, @@ -317,7 +317,7 @@ class Nip47Server( */ fun requestsFilter(since: Long? = null): Filter = Filter( - kinds = listOf(LnZapPaymentRequestEvent.KIND), + kinds = listOf(NwcRequestEvent.KIND), tags = mapOf("p" to listOf(signer.pubKey)), since = since, ) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/README.md b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/README.md index 0d372e1a49..7ee4df5c25 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/README.md +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/README.md @@ -92,8 +92,8 @@ nip47WalletConnect/ ├── NwcErrorCode.kt # Error codes enum + NwcError ├── NwcTransaction.kt # Transaction, state, budget, TLV models ├── NwcInfoEvent.kt # Kind 13194 — wallet capabilities -├── LnZapPaymentRequestEvent.kt # Kind 23194 — client → wallet request -├── LnZapPaymentResponseEvent.kt # Kind 23195 — wallet → client response +├── NwcRequestEvent.kt # Kind 23194 — client → wallet request +├── NwcResponseEvent.kt # Kind 23195 — wallet → client response ├── NwcNotificationEvent.kt # Kind 23197 — wallet → client notification ├── NostrWalletConnectRequestCache.kt # Request decryption cache ├── NostrWalletConnectResponseCache.kt # Response decryption cache @@ -107,8 +107,8 @@ nip47WalletConnect/ | Kind | Class | Direction | Purpose | |-------|------------------------------|-----------------|----------------------| | 13194 | `NwcInfoEvent` | Wallet → Relay | Service capabilities | -| 23194 | `LnZapPaymentRequestEvent` | Client → Wallet | NWC request | -| 23195 | `LnZapPaymentResponseEvent` | Wallet → Client | NWC response | +| 23194 | `NwcRequestEvent` | Client → Wallet | NWC request | +| 23195 | `NwcResponseEvent` | Wallet → Client | NWC response | | 23196 | `NwcNotificationEvent` | Wallet → Client | Notification (NIP-04, legacy) | | 23197 | `NwcNotificationEvent` | Wallet → Client | Notification (NIP-44) | @@ -161,7 +161,7 @@ val clientSigner = NostrSignerInternal( ```kotlin val balanceRequest = GetBalanceMethod.create() -val event = LnZapPaymentRequestEvent.createRequest( +val event = NwcRequestEvent.createRequest( request = balanceRequest, walletServicePubkey = nwcConfig.pubKeyHex, signer = clientSigner, @@ -172,7 +172,7 @@ val event = LnZapPaymentRequestEvent.createRequest( To use NIP-44 encryption instead of NIP-04: ```kotlin -val event = LnZapPaymentRequestEvent.createRequest( +val event = NwcRequestEvent.createRequest( request = GetInfoMethod.create(), walletServicePubkey = nwcConfig.pubKeyHex, signer = clientSigner, @@ -238,7 +238,7 @@ val request: Request = requestEvent.decryptRequest(walletSigner) val balanceResponse = GetBalanceSuccessResponse( GetBalanceSuccessResponse.GetBalanceResult(balance = 2100000L) ) -val responseEvent = LnZapPaymentResponseEvent.createResponse( +val responseEvent = NwcResponseEvent.createResponse( response = balanceResponse, requestEvent = requestEvent, signer = walletSigner, @@ -249,7 +249,7 @@ val errorResponse = NwcErrorResponse( resultType = NwcMethod.PAY_INVOICE, error = NwcError(NwcErrorCode.INSUFFICIENT_BALANCE, "Not enough funds"), ) -val errorEvent = LnZapPaymentResponseEvent.createResponse( +val errorEvent = NwcResponseEvent.createResponse( response = errorResponse, requestEvent = requestEvent, signer = walletSigner, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/cache/NostrWalletConnectRequestCache.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/cache/NostrWalletConnectRequestCache.kt index 59a160871f..b76e0320ac 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/cache/NostrWalletConnectRequestCache.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/cache/NostrWalletConnectRequestCache.kt @@ -23,15 +23,15 @@ package com.vitorpamplona.quartz.nip47WalletConnect.cache import androidx.collection.LruCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.caches.DecryptCache -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request class NostrWalletConnectRequestCache( signer: NostrSigner, ) { private val decryptionCache = - object : LruCache<LnZapPaymentRequestEvent, NWCRequestDecryptCache>(50) { - override fun create(key: LnZapPaymentRequestEvent): NWCRequestDecryptCache? = + object : LruCache<NwcRequestEvent, NWCRequestDecryptCache>(50) { + override fun create(key: NwcRequestEvent): NWCRequestDecryptCache? = if (key.content.isNotBlank() && key.canDecrypt(signer)) { NWCRequestDecryptCache(signer) } else { @@ -39,16 +39,16 @@ class NostrWalletConnectRequestCache( } } - fun cachedRequest(event: LnZapPaymentRequestEvent): Request? = decryptionCache[event]?.cached() + fun cachedRequest(event: NwcRequestEvent): Request? = decryptionCache[event]?.cached() - suspend fun decryptRequest(event: LnZapPaymentRequestEvent) = decryptionCache[event]?.decrypt(event) + suspend fun decryptRequest(event: NwcRequestEvent) = decryptionCache[event]?.decrypt(event) } class NWCRequestDecryptCache( signer: NostrSigner, -) : DecryptCache<LnZapPaymentRequestEvent, Request>(signer) { +) : DecryptCache<NwcRequestEvent, Request>(signer) { override suspend fun decryptAndParse( - event: LnZapPaymentRequestEvent, + event: NwcRequestEvent, signer: NostrSigner, ) = event.decryptRequest(signer) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/cache/NostrWalletConnectResponseCache.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/cache/NostrWalletConnectResponseCache.kt index c291c2c988..818b1041dc 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/cache/NostrWalletConnectResponseCache.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/cache/NostrWalletConnectResponseCache.kt @@ -23,15 +23,15 @@ package com.vitorpamplona.quartz.nip47WalletConnect.cache import androidx.collection.LruCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.caches.DecryptCache -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response class NostrWalletConnectResponseCache( signer: NostrSigner, ) { private val decryptionCache = - object : LruCache<LnZapPaymentResponseEvent, NWCResponseDecryptCache>(50) { - override fun create(key: LnZapPaymentResponseEvent): NWCResponseDecryptCache? = + object : LruCache<NwcResponseEvent, NWCResponseDecryptCache>(50) { + override fun create(key: NwcResponseEvent): NWCResponseDecryptCache? = if (key.content.isNotBlank() && key.canDecrypt(signer)) { NWCResponseDecryptCache(signer) } else { @@ -39,16 +39,16 @@ class NostrWalletConnectResponseCache( } } - fun cachedResponse(event: LnZapPaymentResponseEvent): Response? = decryptionCache[event]?.cached() + fun cachedResponse(event: NwcResponseEvent): Response? = decryptionCache[event]?.cached() - suspend fun decryptResponse(event: LnZapPaymentResponseEvent) = decryptionCache[event]?.decrypt(event) + suspend fun decryptResponse(event: NwcResponseEvent) = decryptionCache[event]?.decrypt(event) } class NWCResponseDecryptCache( signer: NostrSigner, -) : DecryptCache<LnZapPaymentResponseEvent, Response>(signer) { +) : DecryptCache<NwcResponseEvent, Response>(signer) { override suspend fun decryptAndParse( - event: LnZapPaymentResponseEvent, + event: NwcResponseEvent, signer: NostrSigner, ) = event.decrypt(signer) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/events/LnZapPaymentRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/events/NwcRequestEvent.kt similarity index 91% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/events/LnZapPaymentRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/events/NwcRequestEvent.kt index 010ebbef74..fc549558fc 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/events/LnZapPaymentRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/events/NwcRequestEvent.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class LnZapPaymentRequestEvent( +class NwcRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -64,7 +64,7 @@ class LnZapPaymentRequestEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), useNip44: Boolean = false, - ): LnZapPaymentRequestEvent = + ): NwcRequestEvent = createRequest( PayInvoiceMethod.create(lnInvoice), walletServicePubkey, @@ -79,7 +79,7 @@ class LnZapPaymentRequestEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), useNip44: Boolean = false, - ): LnZapPaymentRequestEvent { + ): NwcRequestEvent { val serializedRequest = OptimizedJsonMapper.toJson(request) val tags = @@ -100,3 +100,9 @@ class LnZapPaymentRequestEvent( } } } + +@Deprecated( + "Renamed to NwcRequestEvent. Kind 23194 is the generic NIP-47 wallet request, not only a zap payment.", + ReplaceWith("NwcRequestEvent", "com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent"), +) +typealias LnZapPaymentRequestEvent = NwcRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/events/LnZapPaymentResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/events/NwcResponseEvent.kt similarity index 90% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/events/LnZapPaymentResponseEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/events/NwcResponseEvent.kt index 88b6519e95..7c8769191a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/events/LnZapPaymentResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/events/NwcResponseEvent.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class LnZapPaymentResponseEvent( +class NwcResponseEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -69,11 +69,11 @@ class LnZapPaymentResponseEvent( */ suspend fun createResponse( response: Response, - requestEvent: LnZapPaymentRequestEvent, + requestEvent: NwcRequestEvent, signer: NostrSigner, useNip44: Boolean = false, createdAt: Long = TimeUtils.now(), - ): LnZapPaymentResponseEvent { + ): NwcResponseEvent { val serializedResponse = OptimizedJsonMapper.toJson(response) val clientPubkey = requestEvent.pubKey @@ -95,3 +95,9 @@ class LnZapPaymentResponseEvent( } } } + +@Deprecated( + "Renamed to NwcResponseEvent. Kind 23195 is the generic NIP-47 wallet response, not only a zap payment.", + ReplaceWith("NwcResponseEvent", "com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent"), +) +typealias LnZapPaymentResponseEvent = NwcResponseEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/rpc/NwcTransactionMetadata.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/rpc/NwcTransactionMetadata.kt index d5fca147f8..4a8d5dbf98 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/rpc/NwcTransactionMetadata.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/rpc/NwcTransactionMetadata.kt @@ -179,7 +179,7 @@ class NwcTransactionMetadata( * * NIP-57 sets a zap invoice's `description_hash` to the sha256 of the raw * JSON the LNURL callback received in `nostr=`, and that is - * `LnZapRequestEvent.toJson()` — the exact string used here. A wallet can + * `ZapRequestEvent.toJson()` — the exact string used here. A wallet can * therefore bind this stored event to the invoice it labels, which is what * turns "the client says it paid X" into something the wallet checked. * diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractor.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractor.kt index 97fcb8b7cb..e6176a77fb 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractor.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractor.kt @@ -56,11 +56,11 @@ import com.vitorpamplona.quartz.nip15Marketplace.marketplace.MarketplaceEvent import com.vitorpamplona.quartz.nip15Marketplace.product.ProductEvent import com.vitorpamplona.quartz.nip15Marketplace.stall.StallEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.EditMetadataEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupEditMetadataEvent import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent import com.vitorpamplona.quartz.nip32Labeling.LabelEvent import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent @@ -71,24 +71,24 @@ import com.vitorpamplona.quartz.nip51Lists.appCurationSet.AppCurationSetEvent import com.vitorpamplona.quartz.nip51Lists.articleCurationSet.ArticleCurationSetEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent import com.vitorpamplona.quartz.nip51Lists.mediaStarterPack.MediaStarterPackEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.pictureCurationSet.PictureCurationSetEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent import com.vitorpamplona.quartz.nip51Lists.videoCurationSet.VideoCurationSetEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent @@ -100,13 +100,13 @@ import com.vitorpamplona.quartz.nip69P2pOrderEvents.P2POrderEvent import com.vitorpamplona.quartz.nip71Video.AddressableVideoEvent import com.vitorpamplona.quartz.nip71Video.RegularVideoEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent -import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent +import com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent -import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nipB0WebBookmarks.WebBookmarkEvent import com.vitorpamplona.quartz.nipC0CodeSnippets.CodeSnippetEvent @@ -158,11 +158,11 @@ object SearchFieldExtractor { } } - is LongTextNoteEvent -> { + is LongFormContentEvent -> { tiers(event, event.title(), event.summary(), event.content) } - is WikiNoteEvent -> { + is WikiArticleEvent -> { tiers(event, event.title(), event.summary(), event.content) } @@ -263,7 +263,7 @@ object SearchFieldExtractor { tiers(event, event.title(), null, event.content) } - is CalendarEvent -> { + is CalendarCollectionEvent -> { tiers(event, event.title(), null, event.content) } @@ -352,7 +352,7 @@ object SearchFieldExtractor { // kind 9002 edits the very metadata kind 39000 publishes, so it // splits the same way -- it was the only half of the pair falling // through. Its hashtags() is `t`, carried once by the funnel. - is EditMetadataEvent -> { + is GroupEditMetadataEvent -> { tiers(event, event.name(), event.about(), null) } @@ -360,7 +360,7 @@ object SearchFieldExtractor { tiers(event, event.title(), event.description(), null) } - is FollowListEvent -> { + is StarterPackEvent -> { tiers(event, event.title(), event.description(), null) } @@ -448,7 +448,7 @@ object SearchFieldExtractor { // concatenates the topics INTO the body while the funnel added // them as hashtags. Whether that role is tokenized or kept as // keywords is the backend's call, per IndexableFields. - is ContactCardEvent -> { + is UserAssertionEvent -> { tiers(event, event.petName(), event.summary(), null) } @@ -456,11 +456,11 @@ object SearchFieldExtractor { tiers(event, event.title(), null, null) } - is LabeledBookmarkListEvent -> { + is BookmarkSetEvent -> { tiers(event, event.titleOrName(), event.description(), null) } - is PeopleListEvent -> { + is FollowSetEvent -> { tiers(event, event.titleOrName(), event.description(), null) } @@ -472,7 +472,7 @@ object SearchFieldExtractor { tiers(event, event.title(), null, null) } - is GoalEvent -> { + is ZapGoalEvent -> { tiers(event, null, event.summary(), event.content) } @@ -480,7 +480,7 @@ object SearchFieldExtractor { tiers(event, emptyList(), listOf(event.comment(), event.context()), event.content) } - is FileHeaderEvent -> { + is FileMetadataEvent -> { tiers(event, null, event.summary(), event.content) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchableKinds.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchableKinds.kt index 7c7eb0e954..357eb0c7e2 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchableKinds.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchableKinds.kt @@ -71,7 +71,7 @@ object SearchableKinds { 54, // PodcastEpisodeEvent 818, // WikiMergeRequestEvent 1010, // TextNoteModificationEvent - 1063, // FileHeaderEvent + 1063, // FileMetadataEvent 1065, // FileStorageHeaderEvent 1068, // PollEvent 1111, // CommentEvent @@ -96,20 +96,20 @@ object SearchableKinds { 2004, // TorrentCommentEvent 2473, // BirdDetectionEvent 3302, // ConcordChatEditEvent - 5050, // NIP90TextGenerationRequestEvent - 5100, // NIP90ImageGenerationRequestEvent + 5050, // DvmTextGenerationRequestEvent + 5100, // DvmImageGenerationRequestEvent 5129, // NappletSnapshotEvent - 5250, // NIP90TextToSpeechRequestEvent - 5302, // NIP90ContentSearchRequestEvent - 5303, // NIP90PeopleSearchRequestEvent + 5250, // DvmTextToSpeechRequestEvent + 5302, // DvmContentSearchRequestEvent + 5303, // DvmPeopleSearchRequestEvent 6969, // ZapPollEvent 7516, // GeocacheFoundLogEvent 8333, // OnchainZapEvent - 9002, // EditMetadataEvent - 9041, // GoalEvent + 9002, // GroupEditMetadataEvent + 9041, // ZapGoalEvent 9321, // NutzapEvent - 9734, // LnZapRequestEvent - 9735, // LnZapEvent + 9734, // ZapRequestEvent + 9735, // ZapReceiptEvent 9736, // Bolt12ZapEvent 9737, // Bolt12ZapIntentEvent 9802, // HighlightEvent @@ -122,10 +122,10 @@ object SearchableKinds { 12473, // BirdexEvent 15128, // RootSiteEvent 15129, // RootNappletEvent - 30000, // PeopleListEvent + 30000, // FollowSetEvent 30001, // OldBookmarkListEvent 30002, // RelaySetEvent - 30003, // LabeledBookmarkListEvent + 30003, // BookmarkSetEvent 30004, // ArticleCurationSetEvent 30005, // VideoCurationSetEvent 30006, // PictureCurationSetEvent @@ -135,7 +135,7 @@ object SearchableKinds { 30018, // ProductEvent 30019, // MarketplaceEvent 30020, // AuctionEvent - 30023, // LongTextNoteEvent + 30023, // LongFormContentEvent 30030, // EmojiPackEvent 30040, // PublicationIndexEvent 30041, // PublicationContentEvent @@ -153,8 +153,8 @@ object SearchableKinds { 30311, // LiveActivitiesEvent 30312, // MeetingSpaceEvent 30313, // MeetingRoomEvent - 30315, // StatusEvent - 30382, // ContactCardEvent + 30315, // UserStatusEvent + 30382, // UserAssertionEvent 30392, // UserTrustedListEvent 30393, // EventTrustedListEvent 30394, // AddressableTrustedListEvent @@ -163,14 +163,14 @@ object SearchableKinds { 30617, // GitRepositoryEvent 30620, // WorkflowDefEvent 30817, // NipTextEvent - 30818, // WikiNoteEvent + 30818, // WikiArticleEvent 31337, // AudioTrackEvent 31871, // AttestationEvent 31872, // AttestationRequestEvent 31873, // AttestorRecommendationEvent 31922, // CalendarDateSlotEvent 31923, // CalendarTimeSlotEvent - 31924, // CalendarEvent + 31924, // CalendarCollectionEvent 31925, // CalendarRSVPEvent 31987, // RelayReviewEvent 31990, // AppDefinitionEvent @@ -179,8 +179,8 @@ object SearchableKinds { 33401, // ExerciseTemplateEvent 33863, // FundraiserEvent 34139, // MusicPlaylistEvent - 34235, // VideoHorizontalEvent - 34236, // VideoVerticalEvent + 34235, // AddressableNormalVideoEvent + 34236, // AddressableShortVideoEvent 34259, // EntityRatingEvent 34550, // CommunityDefinitionEvent 35128, // NamedSiteEvent @@ -192,7 +192,7 @@ object SearchableKinds { 38192, // Ps1SaveEvent 38383, // P2POrderEvent 39000, // GroupMetadataEvent - 39089, // FollowListEvent + 39089, // StarterPackEvent 39092, // MediaStarterPackEvent 39307, // TextTrackEvent 39701, // WebBookmarkEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/FollowListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/StarterPackEvent.kt similarity index 90% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/FollowListEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/StarterPackEvent.kt index edcc791377..c38841df66 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/FollowListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/StarterPackEvent.kt @@ -42,7 +42,7 @@ import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @Immutable -class FollowListEvent( +class StarterPackEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -99,7 +99,7 @@ class FollowListEvent( signer: NostrSigner, dTag: String = Uuid.random().toString(), createdAt: Long = TimeUtils.now(), - ): FollowListEvent = + ): StarterPackEvent = create( name = name, people = listOf(person), @@ -109,11 +109,11 @@ class FollowListEvent( ) suspend fun addUsers( - earlierVersion: FollowListEvent, + earlierVersion: StarterPackEvent, people: List<UserTag>, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): FollowListEvent = + ): StarterPackEvent = resign( content = earlierVersion.content, tags = earlierVersion.tags.plus(people.map { it.toTagArray() }), @@ -122,18 +122,18 @@ class FollowListEvent( ) suspend fun add( - earlierVersion: FollowListEvent, + earlierVersion: StarterPackEvent, person: UserTag, signer: NostrSigner, createdAt: Long = TimeUtils.now(), ) = addUsers(earlierVersion, listOf(person), signer, createdAt) suspend fun remove( - earlierVersion: FollowListEvent, + earlierVersion: StarterPackEvent, person: UserTag, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): FollowListEvent = + ): StarterPackEvent = resign( content = earlierVersion.content, tags = earlierVersion.tags.remove(person.toTagIdOnly()), @@ -146,7 +146,7 @@ class FollowListEvent( tags: Array<Array<String>>, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): FollowListEvent { + ): StarterPackEvent { val newTags = tags return signer.sign(createdAt, KIND, newTags, content) @@ -159,7 +159,7 @@ class FollowListEvent( signer: NostrSigner, dTag: String = Uuid.random().toString(), createdAt: Long = TimeUtils.now(), - ): FollowListEvent { + ): StarterPackEvent { val template = build(name, people, dTag, createdAt) return signer.sign(template) } @@ -170,7 +170,7 @@ class FollowListEvent( people: List<UserTag> = emptyList(), dTag: String = Uuid.random().toString(), createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<FollowListEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<StarterPackEvent>.() -> Unit = {}, ) = eventTemplate( kind = KIND, description = "", @@ -184,3 +184,9 @@ class FollowListEvent( } } } + +@Deprecated( + "Renamed to StarterPackEvent. Kind 39089 is a NIP-51 starter pack. The NIP-02 follow list is kind 3 (ContactListEvent).", + ReplaceWith("StarterPackEvent", "com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent"), +) +typealias FollowListEvent = StarterPackEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/TagArrayBuilderExt.kt index 60eac2631c..6b5cf9bc13 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/TagArrayBuilderExt.kt @@ -28,24 +28,24 @@ import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag -fun TagArrayBuilder<FollowListEvent>.title(title: String) = addUnique(TitleTag.assemble(title)) +fun TagArrayBuilder<StarterPackEvent>.title(title: String) = addUnique(TitleTag.assemble(title)) -fun TagArrayBuilder<FollowListEvent>.description(desc: String) = addUnique(DescriptionTag.assemble(desc)) +fun TagArrayBuilder<StarterPackEvent>.description(desc: String) = addUnique(DescriptionTag.assemble(desc)) -fun TagArrayBuilder<FollowListEvent>.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) +fun TagArrayBuilder<StarterPackEvent>.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) -fun TagArrayBuilder<FollowListEvent>.people(peoples: List<UserTag>) = addAll(peoples.map { it.toTagArray() }) +fun TagArrayBuilder<StarterPackEvent>.people(peoples: List<UserTag>) = addAll(peoples.map { it.toTagArray() }) -fun TagArrayBuilder<FollowListEvent>.person(person: UserTag) = add(person.toTagArray()) +fun TagArrayBuilder<StarterPackEvent>.person(person: UserTag) = add(person.toTagArray()) -fun TagArrayBuilder<FollowListEvent>.person( +fun TagArrayBuilder<StarterPackEvent>.person( pubkey: HexKey, relayHint: NormalizedRelayUrl?, ) = add(UserTag.assemble(pubkey, relayHint)) -fun TagArrayBuilder<FollowListEvent>.personFirst( +fun TagArrayBuilder<StarterPackEvent>.personFirst( pubkey: HexKey, relayHint: NormalizedRelayUrl?, ) = addFirst(UserTag.assemble(pubkey, relayHint)) -fun TagArrayBuilder<FollowListEvent>.removePerson(pubkey: HexKey) = remove(UserTag.TAG_NAME, pubkey) +fun TagArrayBuilder<StarterPackEvent>.removePerson(pubkey: HexKey) = remove(UserTag.TAG_NAME, pubkey) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/HashtagListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/InterestListEvent.kt similarity index 90% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/HashtagListEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/InterestListEvent.kt index 67af6cddc7..950fb6c7a9 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/HashtagListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/InterestListEvent.kt @@ -41,7 +41,7 @@ import com.vitorpamplona.quartz.nip51Lists.removeIgnoreCase import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class HashtagListEvent( +class InterestListEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -51,7 +51,7 @@ class HashtagListEvent( ) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig), DiffableEvent<HashtagListDiff> { override fun diffFrom(older: Event): HashtagListDiff? { - if (older !is HashtagListEvent || older.pubKey != pubKey || older.dTag() != dTag()) return null + if (older !is InterestListEvent || older.pubKey != pubKey || older.dTag() != dTag()) return null return HashtagListDiff( ListDiff.of(older.publicHashtags(), publicHashtags(), { it.lowercase() }), privateItemsChangeFrom(older), @@ -83,7 +83,7 @@ class HashtagListEvent( isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): HashtagListEvent = + ): InterestListEvent = if (isPrivate) { create( publicHashtags = emptyList(), @@ -101,7 +101,7 @@ class HashtagListEvent( } suspend fun add( - earlierVersion: HashtagListEvent, + earlierVersion: InterestListEvent, hashtag: String, isPrivate: Boolean, signer: NostrSigner, @@ -115,12 +115,12 @@ class HashtagListEvent( ) suspend fun add( - earlierVersion: HashtagListEvent, + earlierVersion: InterestListEvent, hashtags: List<String>, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): HashtagListEvent { + ): InterestListEvent { val hashtags = hashtags.map { HashtagTag.assemble(it) } return if (isPrivate) { val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() @@ -141,11 +141,11 @@ class HashtagListEvent( } suspend fun remove( - earlierVersion: HashtagListEvent, + earlierVersion: InterestListEvent, hashtag: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): HashtagListEvent { + ): InterestListEvent { val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() return resign( privateTags = privateTags.removeIgnoreCase(HashtagTag.assemble(hashtag)), @@ -172,7 +172,7 @@ class HashtagListEvent( tags: TagArray, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): HashtagListEvent { + ): InterestListEvent { val newTags = tags return signer.sign(createdAt, KIND, newTags, content) @@ -183,7 +183,7 @@ class HashtagListEvent( privateHashtags: List<String> = emptyList(), signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): HashtagListEvent { + ): InterestListEvent { val template = build(publicHashtags, privateHashtags, signer, createdAt) return signer.sign(template) } @@ -193,7 +193,7 @@ class HashtagListEvent( privateHashtags: List<String> = emptyList(), signer: NostrSignerSync, createdAt: Long = TimeUtils.now(), - ): HashtagListEvent { + ): InterestListEvent { val privateTagArray = publicHashtags.map { HashtagTag.assemble(it) }.toTypedArray() val publicTagArray = privateHashtags.map { HashtagTag.assemble(it) }.toTypedArray() return signer.signNip51List(createdAt, KIND, publicTagArray, privateTagArray) @@ -204,8 +204,8 @@ class HashtagListEvent( privateHashtags: List<String> = emptyList(), signer: NostrSigner, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<HashtagListEvent>.() -> Unit = {}, - ) = eventTemplate<HashtagListEvent>( + initializer: TagArrayBuilder<InterestListEvent>.() -> Unit = {}, + ) = eventTemplate<InterestListEvent>( kind = KIND, description = PrivateTagsInContent.encryptNip44(privateHashtags.map { HashtagTag.assemble(it) }.toTypedArray(), signer), createdAt = createdAt, @@ -216,3 +216,9 @@ class HashtagListEvent( } } } + +@Deprecated( + "Renamed to InterestListEvent. NIP-51 names kind 10015 the interests list.", + ReplaceWith("InterestListEvent", "com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent"), +) +typealias HashtagListEvent = InterestListEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/TagArrayBuilderExt.kt index 5d271de593..70977ca9b7 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/TagArrayBuilderExt.kt @@ -23,6 +23,6 @@ package com.vitorpamplona.quartz.nip51Lists.hashtagList import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag -fun TagArrayBuilder<HashtagListEvent>.followHashTag(hashtag: String) = add(HashtagTag.assemble(hashtag)) +fun TagArrayBuilder<InterestListEvent>.followHashTag(hashtag: String) = add(HashtagTag.assemble(hashtag)) -fun TagArrayBuilder<HashtagListEvent>.hashtags(hashtags: List<String>) = addAll(hashtags.map { HashtagTag.assemble(it) }) +fun TagArrayBuilder<InterestListEvent>.hashtags(hashtags: List<String>) = addAll(hashtags.map { HashtagTag.assemble(it) }) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/LabeledBookmarkListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/BookmarkSetEvent.kt similarity index 94% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/LabeledBookmarkListEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/BookmarkSetEvent.kt index 2edbdca9e1..01494372a3 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/LabeledBookmarkListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/BookmarkSetEvent.kt @@ -49,7 +49,7 @@ import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @Immutable -class LabeledBookmarkListEvent( +class BookmarkSetEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -113,7 +113,7 @@ class LabeledBookmarkListEvent( optionalListDescription: String? = null, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): LabeledBookmarkListEvent = + ): BookmarkSetEvent = if (isPrivate) { create( title = title, @@ -135,12 +135,12 @@ class LabeledBookmarkListEvent( } suspend fun addBookmark( - earlierVersion: LabeledBookmarkListEvent, + earlierVersion: BookmarkSetEvent, bookmarkIdTag: BookmarkIdTag, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): LabeledBookmarkListEvent = + ): BookmarkSetEvent = if (isPrivate) { val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() resign( @@ -159,12 +159,12 @@ class LabeledBookmarkListEvent( } suspend fun moveBookmark( - earlierVersion: LabeledBookmarkListEvent, + earlierVersion: BookmarkSetEvent, bookmarkIdTag: BookmarkIdTag, isCurrentlyPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): LabeledBookmarkListEvent = + ): BookmarkSetEvent = if (isCurrentlyPrivate) { val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() resign( @@ -184,12 +184,12 @@ class LabeledBookmarkListEvent( } suspend fun removeBookmark( - earlierVersion: LabeledBookmarkListEvent, + earlierVersion: BookmarkSetEvent, bookmarkIdTag: BookmarkIdTag, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): LabeledBookmarkListEvent = + ): BookmarkSetEvent = if (isPrivate) { val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() resign( @@ -208,11 +208,11 @@ class LabeledBookmarkListEvent( } suspend fun modifyName( - earlierVersion: LabeledBookmarkListEvent, + earlierVersion: BookmarkSetEvent, newTitle: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): LabeledBookmarkListEvent { + ): BookmarkSetEvent { val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() val currentTitle = earlierVersion.tags.first { it[0] == NameTag.TAG_NAME || it[0] == TitleTag.TAG_NAME } val newTitleTag = @@ -231,11 +231,11 @@ class LabeledBookmarkListEvent( } suspend fun modifyDescription( - earlierVersion: LabeledBookmarkListEvent, + earlierVersion: BookmarkSetEvent, newDescription: String?, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): LabeledBookmarkListEvent? { + ): BookmarkSetEvent? { val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() val currentDescriptionTag = earlierVersion.tags.firstOrNull { it[0] == DescriptionTag.TAG_NAME } val currentDescription = currentDescriptionTag?.get(1) @@ -288,7 +288,7 @@ class LabeledBookmarkListEvent( tags: TagArray, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): LabeledBookmarkListEvent { + ): BookmarkSetEvent { val newTags = tags return signer.sign(createdAt, KIND, newTags, content) @@ -304,7 +304,7 @@ class LabeledBookmarkListEvent( dTag: String = Uuid.random().toString(), signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): LabeledBookmarkListEvent { + ): BookmarkSetEvent { val template = build(title, publicBookmarks, privateBookmarks, signer, dTag, createdAt) { if (description != null) description(description) @@ -321,7 +321,7 @@ class LabeledBookmarkListEvent( signer: NostrSigner, dTag: String = Uuid.random().toString(), createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<LabeledBookmarkListEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<BookmarkSetEvent>.() -> Unit = {}, ) = eventTemplate( kind = KIND, description = PrivateTagsInContent.encryptNip44(privateBookmarks.map { it.toTagArray() }.toTypedArray(), signer), @@ -335,3 +335,9 @@ class LabeledBookmarkListEvent( } } } + +@Deprecated( + "Renamed to BookmarkSetEvent. NIP-51 names kind 30003 bookmark sets.", + ReplaceWith("BookmarkSetEvent", "com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent"), +) +typealias LabeledBookmarkListEvent = BookmarkSetEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/TagArrayBuilderExt.kt index 109d81a516..5754f4f9f2 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/TagArrayBuilderExt.kt @@ -28,12 +28,12 @@ import com.vitorpamplona.quartz.nip51Lists.tags.NameTag import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag @Deprecated("NIP-51 has deprecated name. Use title instead", ReplaceWith("title()")) -fun TagArrayBuilder<LabeledBookmarkListEvent>.name(name: String) = addUnique(NameTag.assemble(name)) +fun TagArrayBuilder<BookmarkSetEvent>.name(name: String) = addUnique(NameTag.assemble(name)) -fun TagArrayBuilder<LabeledBookmarkListEvent>.title(title: String) = addUnique(TitleTag.assemble(title)) +fun TagArrayBuilder<BookmarkSetEvent>.title(title: String) = addUnique(TitleTag.assemble(title)) -fun TagArrayBuilder<LabeledBookmarkListEvent>.bookmarks(bookmarks: List<BookmarkIdTag>) = addAll(bookmarks.map { it.toTagArray() }) +fun TagArrayBuilder<BookmarkSetEvent>.bookmarks(bookmarks: List<BookmarkIdTag>) = addAll(bookmarks.map { it.toTagArray() }) -fun TagArrayBuilder<LabeledBookmarkListEvent>.description(listDescription: String) = addUnique(DescriptionTag.assemble(listDescription)) +fun TagArrayBuilder<BookmarkSetEvent>.description(listDescription: String) = addUnique(DescriptionTag.assemble(listDescription)) -fun TagArrayBuilder<LabeledBookmarkListEvent>.image(url: String) = addUnique(ImageTag.assemble(url)) +fun TagArrayBuilder<BookmarkSetEvent>.image(url: String) = addUnique(ImageTag.assemble(url)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/FollowSetEvent.kt similarity index 93% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/FollowSetEvent.kt index 906f06587e..d10d7de69b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/FollowSetEvent.kt @@ -48,7 +48,7 @@ import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @Immutable -class PeopleListEvent( +class FollowSetEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -121,7 +121,7 @@ class PeopleListEvent( signer: NostrSigner, dTag: String = Uuid.random().toString(), createdAt: Long = TimeUtils.now(), - ): PeopleListEvent = + ): FollowSetEvent = if (isPrivate) { create( title = title, @@ -143,12 +143,12 @@ class PeopleListEvent( } suspend fun add( - earlierVersion: PeopleListEvent, + earlierVersion: FollowSetEvent, person: UserTag, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): PeopleListEvent = + ): FollowSetEvent = if (isPrivate) { val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() resign( @@ -167,12 +167,12 @@ class PeopleListEvent( } suspend fun addFirst( - earlierVersion: PeopleListEvent, + earlierVersion: FollowSetEvent, person: UserTag, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): PeopleListEvent = + ): FollowSetEvent = if (isPrivate) { val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() resign( @@ -191,11 +191,11 @@ class PeopleListEvent( } suspend fun remove( - earlierVersion: PeopleListEvent, + earlierVersion: FollowSetEvent, person: MuteTag, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): PeopleListEvent { + ): FollowSetEvent { val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() return resign( @@ -207,11 +207,11 @@ class PeopleListEvent( } suspend fun removeAll( - earlierVersion: PeopleListEvent, + earlierVersion: FollowSetEvent, persons: List<MuteTag>, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): PeopleListEvent { + ): FollowSetEvent { val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() val ids = persons.map { it.toTagIdOnly() } @@ -224,12 +224,12 @@ class PeopleListEvent( } suspend fun remove( - earlierVersion: PeopleListEvent, + earlierVersion: FollowSetEvent, person: UserTag, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): PeopleListEvent { + ): FollowSetEvent { if (isPrivate) { val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() return resign( @@ -265,7 +265,7 @@ class PeopleListEvent( tags: Array<Array<String>>, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): PeopleListEvent { + ): FollowSetEvent { val newTags = tags return signer.sign(createdAt, KIND, newTags, content) @@ -279,7 +279,7 @@ class PeopleListEvent( signer: NostrSigner, dTag: String = Uuid.random().toString(), createdAt: Long = TimeUtils.now(), - ): PeopleListEvent { + ): FollowSetEvent { val template = build(title, publicMembers, privateMembers, signer, dTag, createdAt) return signer.sign(template) } @@ -292,8 +292,8 @@ class PeopleListEvent( signer: NostrSigner, dTag: String = Uuid.random().toString(), createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<PeopleListEvent>.() -> Unit = {}, - ) = eventTemplate<PeopleListEvent>( + initializer: TagArrayBuilder<FollowSetEvent>.() -> Unit = {}, + ) = eventTemplate<FollowSetEvent>( kind = KIND, description = PrivateTagsInContent.encryptNip44(privateMembers.map { it.toTagArray() }.toTypedArray(), signer), createdAt = createdAt, @@ -313,7 +313,7 @@ class PeopleListEvent( privateMembers: List<UserTag> = emptyList(), signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): PeopleListEvent { + ): FollowSetEvent { val newListTemplate = build( title = title, @@ -336,7 +336,7 @@ class PeopleListEvent( privateMembers: List<UserTag> = emptyList(), signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): PeopleListEvent { + ): FollowSetEvent { val cloneTemplate = build( title = title, @@ -358,7 +358,7 @@ class PeopleListEvent( isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): PeopleListEvent = + ): FollowSetEvent = create( title = title, person = UserTag(pubKey = pubKeyHex), @@ -368,13 +368,13 @@ class PeopleListEvent( ) suspend fun addUser( - earlierVersion: PeopleListEvent, + earlierVersion: FollowSetEvent, pubKeyHex: String, relayHint: NormalizedRelayUrl?, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): PeopleListEvent = + ): FollowSetEvent = add( earlierVersion = earlierVersion, person = UserTag(pubKey = pubKeyHex, relayHint), @@ -384,13 +384,13 @@ class PeopleListEvent( ) suspend fun addUserFirst( - earlierVersion: PeopleListEvent, + earlierVersion: FollowSetEvent, pubKeyHex: String, relayHint: NormalizedRelayUrl?, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): PeopleListEvent = + ): FollowSetEvent = addFirst( earlierVersion = earlierVersion, person = UserTag(pubKey = pubKeyHex, relayHint), @@ -400,12 +400,12 @@ class PeopleListEvent( ) suspend fun removeUser( - earlierVersion: PeopleListEvent, + earlierVersion: FollowSetEvent, pubKeyHex: String, isUserPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): PeopleListEvent = + ): FollowSetEvent = remove( earlierVersion = earlierVersion, person = UserTag(pubKey = pubKeyHex), @@ -415,3 +415,9 @@ class PeopleListEvent( ) } } + +@Deprecated( + "Renamed to FollowSetEvent. NIP-51 names kind 30000 follow sets.", + ReplaceWith("FollowSetEvent", "com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent"), +) +typealias PeopleListEvent = FollowSetEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/TagArrayBuilderExt.kt index 5129023f76..9a07db2fec 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/TagArrayBuilderExt.kt @@ -28,12 +28,12 @@ import com.vitorpamplona.quartz.nip51Lists.tags.NameTag import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag @Deprecated("NIP-51 has deprecated name. Use title instead", ReplaceWith("title()")) -fun TagArrayBuilder<PeopleListEvent>.name(name: String) = addUnique(NameTag.assemble(name)) +fun TagArrayBuilder<FollowSetEvent>.name(name: String) = addUnique(NameTag.assemble(name)) -fun TagArrayBuilder<PeopleListEvent>.title(name: String) = addUnique(TitleTag.assemble(name)) +fun TagArrayBuilder<FollowSetEvent>.title(name: String) = addUnique(TitleTag.assemble(name)) -fun TagArrayBuilder<PeopleListEvent>.description(desc: String) = addUnique(DescriptionTag.assemble(desc)) +fun TagArrayBuilder<FollowSetEvent>.description(desc: String) = addUnique(DescriptionTag.assemble(desc)) -fun TagArrayBuilder<PeopleListEvent>.image(url: String) = addUnique(ImageTag.assemble(url)) +fun TagArrayBuilder<FollowSetEvent>.image(url: String) = addUnique(ImageTag.assemble(url)) -fun TagArrayBuilder<PeopleListEvent>.peoples(peoples: List<UserTag>) = addAll(peoples.map { it.toTagArray() }) +fun TagArrayBuilder<FollowSetEvent>.peoples(peoples: List<UserTag>) = addAll(peoples.map { it.toTagArray() }) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/RelayFeedsListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/FavoriteRelayListEvent.kt similarity index 88% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/RelayFeedsListEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/FavoriteRelayListEvent.kt index a758db06d1..29179f5d54 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/RelayFeedsListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/FavoriteRelayListEvent.kt @@ -44,7 +44,7 @@ import com.vitorpamplona.quartz.nip51Lists.splitRelayListUpdate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class RelayFeedsListEvent( +class FavoriteRelayListEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -54,7 +54,7 @@ class RelayFeedsListEvent( ) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig), DiffableEvent<RelayListDiff> { override fun diffFrom(older: Event): RelayListDiff? { - if (older !is RelayFeedsListEvent || older.pubKey != pubKey || older.dTag() != dTag()) return null + if (older !is FavoriteRelayListEvent || older.pubKey != pubKey || older.dTag() != dTag()) return null return RelayListDiff(ListDiff.of(older.publicRelays(), publicRelays(), { it }), privateItemsChangeFrom(older)) } @@ -74,11 +74,11 @@ class RelayFeedsListEvent( fun createAddressTag(pubKey: HexKey): String = Address.Companion.assemble(KIND, pubKey, "") suspend fun updateRelayList( - earlierVersion: RelayFeedsListEvent, + earlierVersion: FavoriteRelayListEvent, relays: List<NormalizedRelayUrl>, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): RelayFeedsListEvent { + ): FavoriteRelayListEvent { val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() // Keeps public relays public and private relays private: rewriting them all as @@ -95,7 +95,7 @@ class RelayFeedsListEvent( relays: List<NormalizedRelayUrl>, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): RelayFeedsListEvent { + ): FavoriteRelayListEvent { val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray() return signer.signNip51List(createdAt, KIND, emptyArray(), privateTagArray) } @@ -104,7 +104,7 @@ class RelayFeedsListEvent( relays: List<NormalizedRelayUrl>, signer: NostrSignerSync, createdAt: Long = TimeUtils.now(), - ): RelayFeedsListEvent { + ): FavoriteRelayListEvent { val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray() return signer.signNip51List(createdAt, KIND, emptyArray(), privateTagArray) } @@ -114,8 +114,8 @@ class RelayFeedsListEvent( privateRelays: List<NormalizedRelayUrl> = emptyList(), signer: NostrSigner, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<RelayFeedsListEvent>.() -> Unit = {}, - ) = eventTemplate<RelayFeedsListEvent>( + initializer: TagArrayBuilder<FavoriteRelayListEvent>.() -> Unit = {}, + ) = eventTemplate<FavoriteRelayListEvent>( kind = KIND, description = PrivateTagsInContent.encryptNip44(privateRelays.map { RelayTag.assemble(it) }.toTypedArray(), signer), createdAt = createdAt, @@ -126,3 +126,9 @@ class RelayFeedsListEvent( } } } + +@Deprecated( + "Renamed to FavoriteRelayListEvent. NIP-51 names kind 10012 the favorite relays list.", + ReplaceWith("FavoriteRelayListEvent", "com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent"), +) +typealias RelayFeedsListEvent = FavoriteRelayListEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt index ecbc663eea..c2dfb98574 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt @@ -27,9 +27,9 @@ import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.tags.NameTag @@ -52,4 +52,4 @@ fun TagArrayBuilder<PrivateOutboxRelayListEvent>.privateRelays(relays: List<Norm fun TagArrayBuilder<RelaySetEvent>.relaySet(relays: List<NormalizedRelayUrl>) = addAll(relays.map { RelayTag.assemble(it) }) -fun TagArrayBuilder<RelayFeedsListEvent>.relayFeeds(relays: List<NormalizedRelayUrl>) = addAll(relays.map { RelayTag.assemble(it) }) +fun TagArrayBuilder<FavoriteRelayListEvent>.relayFeeds(relays: List<NormalizedRelayUrl>) = addAll(relays.map { RelayTag.assemble(it) }) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt index 99bb13f527..b3c2dee634 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt @@ -21,11 +21,18 @@ package com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAssetEvent +import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.appId +import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.assets +import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.channel +import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.version import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArray 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.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate @@ -42,6 +49,15 @@ import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid +/** + * Kind 30063: the artifacts of one software release. + * + * Two specs publish this kind and both are parsed here: + * - NIP-51 "release artifact set": a `title`, a `description` and `e`/`a` items. + * - NIP-82 "software release": `d = <app-id>@<version>`, `i` (app id), `version`, + * `c` (channel), `e` tags to its kind 3063 assets, and the release notes as content. + * Use `isNip82SoftwareRelease()` to tell them apart. + */ @Immutable class ReleaseArtifactSetEvent( id: HexKey, @@ -50,7 +66,7 @@ class ReleaseArtifactSetEvent( tags: Array<Array<String>>, content: String, sig: HexKey, -) : com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig), +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig), EventHintProvider, AddressHintProvider, SearchableEvent { @@ -77,9 +93,48 @@ class ReleaseArtifactSetEvent( fun items(): List<BookmarkIdTag> = tags.mapNotNull(BookmarkIdTag::parse) + /** NIP-82: the application identifier (`i` tag). */ + fun appId() = tags.appId() + + /** NIP-82: the release version. */ + fun version() = tags.version() + + /** NIP-82: the release channel (`c` tag). */ + fun channel() = tags.channel() + + /** NIP-82: the kind 3063 assets of this release. */ + fun assets() = tags.assets() + + /** NIP-82: the release notes. */ + fun releaseNotes() = content + companion object { const val KIND = 30063 + /** NIP-82 requires `d = <app-id>@<version>`. */ + fun buildSoftwareReleaseDTag( + appId: String, + version: String, + ) = "$appId@$version" + + /** Builds a NIP-82 software release. */ + fun buildSoftwareRelease( + appId: String, + version: String, + channel: String, + assets: List<EventHintBundle<SoftwareAssetEvent>>, + releaseNotes: String = "", + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder<ReleaseArtifactSetEvent>.() -> Unit = {}, + ) = eventTemplate(KIND, releaseNotes, createdAt) { + dTag(buildSoftwareReleaseDTag(appId, version)) + appId(appId) + version(version) + channel(channel) + assets(assets) + initializer() + } + fun createAddress( pubKey: HexKey, dTag: String, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip52Calendar/calendar/CalendarEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip52Calendar/calendar/CalendarCollectionEvent.kt similarity index 89% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip52Calendar/calendar/CalendarEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip52Calendar/calendar/CalendarCollectionEvent.kt index 5bbebfda64..f9347efae3 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip52Calendar/calendar/CalendarEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip52Calendar/calendar/CalendarCollectionEvent.kt @@ -37,7 +37,7 @@ import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @Immutable -class CalendarEvent( +class CalendarCollectionEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -76,7 +76,7 @@ class CalendarEvent( content: String = "", dTag: String = Uuid.random().toString(), createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<CalendarEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<CalendarCollectionEvent>.() -> Unit = {}, ) = eventTemplate(KIND, content, createdAt) { dTag(dTag) title(title) @@ -84,3 +84,9 @@ class CalendarEvent( } } } + +@Deprecated( + "Renamed to CalendarCollectionEvent. NIP-52 kind 31924 is a calendar (a collection of calendar events), not a calendar event.", + ReplaceWith("CalendarCollectionEvent", "com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent"), +) +typealias CalendarEvent = CalendarCollectionEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip52Calendar/calendar/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip52Calendar/calendar/TagArrayBuilderExt.kt index 5e0a174929..2376bb6f97 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip52Calendar/calendar/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip52Calendar/calendar/TagArrayBuilderExt.kt @@ -23,6 +23,6 @@ package com.vitorpamplona.quartz.nip52Calendar.calendar import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag -// CalendarEvent builder extensions +// CalendarCollectionEvent builder extensions -fun TagArrayBuilder<CalendarEvent>.title(title: String) = addUnique(TitleTag.assemble(title)) +fun TagArrayBuilder<CalendarCollectionEvent>.title(title: String) = addUnique(TitleTag.assemble(title)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/TagArrayBuilderExt.kt index fc7bcb353a..eb73b29190 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/TagArrayBuilderExt.kt @@ -26,10 +26,10 @@ import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag -fun TagArrayBuilder<WikiNoteEvent>.title(title: String) = addUnique(TitleTag.assemble(title)) +fun TagArrayBuilder<WikiArticleEvent>.title(title: String) = addUnique(TitleTag.assemble(title)) -fun TagArrayBuilder<WikiNoteEvent>.summary(summary: String) = addUnique(SummaryTag.assemble(summary)) +fun TagArrayBuilder<WikiArticleEvent>.summary(summary: String) = addUnique(SummaryTag.assemble(summary)) -fun TagArrayBuilder<WikiNoteEvent>.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) +fun TagArrayBuilder<WikiArticleEvent>.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) -fun TagArrayBuilder<WikiNoteEvent>.publishedAt(publishedAt: Long) = addUnique(PublishedAtTag.assemble(publishedAt)) +fun TagArrayBuilder<WikiArticleEvent>.publishedAt(publishedAt: Long) = addUnique(PublishedAtTag.assemble(publishedAt)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiArticleEvent.kt similarity index 95% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiArticleEvent.kt index 42cae11b8a..ce75d16efb 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiArticleEvent.kt @@ -62,7 +62,7 @@ import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @Immutable -class WikiNoteEvent( +class WikiArticleEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -177,8 +177,8 @@ class WikiNoteEvent( publishedAt: Long? = null, dTag: String = Uuid.random().toString(), createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<WikiNoteEvent>.() -> Unit = {}, - ): EventTemplate<WikiNoteEvent> = + initializer: TagArrayBuilder<WikiArticleEvent>.() -> Unit = {}, + ): EventTemplate<WikiArticleEvent> = eventTemplate(KIND, description, createdAt) { dTag(dTag) @@ -191,3 +191,9 @@ class WikiNoteEvent( } } } + +@Deprecated( + "Renamed to WikiArticleEvent. NIP-54 names kind 30818 the wiki article.", + ReplaceWith("WikiArticleEvent", "com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent"), +) +typealias WikiNoteEvent = WikiArticleEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/IPrivateZapsDecryptionCache.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/IPrivateZapsDecryptionCache.kt index 2b717721b2..bae51c5c4f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/IPrivateZapsDecryptionCache.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/IPrivateZapsDecryptionCache.kt @@ -25,7 +25,7 @@ package com.vitorpamplona.quartz.nip57Zaps * Used by Note.kt for checking private zap status. */ interface IPrivateZapsDecryptionCache { - fun cachedPrivateZap(event: LnZapRequestEvent): LnZapPrivateEvent? + fun cachedPrivateZap(event: ZapRequestEvent): LnZapPrivateEvent? - suspend fun decryptPrivateZap(event: LnZapRequestEvent): LnZapPrivateEvent? + suspend fun decryptPrivateZap(event: ZapRequestEvent): LnZapPrivateEvent? } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapCache.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapCache.kt index 129d64a346..57eb0a796c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapCache.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapCache.kt @@ -28,8 +28,8 @@ class PrivateZapCache( signer: NostrSigner, ) : IPrivateZapsDecryptionCache { private val decryptionCache = - object : LruCache<LnZapRequestEvent, PrivateZapDecryptCache>(1000) { - override fun create(key: LnZapRequestEvent): PrivateZapDecryptCache? { + object : LruCache<ZapRequestEvent, PrivateZapDecryptCache>(1000) { + override fun create(key: ZapRequestEvent): PrivateZapDecryptCache? { val zappedAuthor = key.zappedAuthor().firstOrNull() return if (key.isPrivateZap() && zappedAuthor != null) { PrivateZapDecryptCache(signer) @@ -39,20 +39,20 @@ class PrivateZapCache( } } - fun delete(event: LnZapRequestEvent) { + fun delete(event: ZapRequestEvent) { decryptionCache.remove(event) } - override fun cachedPrivateZap(event: LnZapRequestEvent): LnZapPrivateEvent? = decryptionCache[event]?.cached() + override fun cachedPrivateZap(event: ZapRequestEvent): LnZapPrivateEvent? = decryptionCache[event]?.cached() - override suspend fun decryptPrivateZap(event: LnZapRequestEvent) = decryptionCache[event]?.decrypt(event) + override suspend fun decryptPrivateZap(event: ZapRequestEvent) = decryptionCache[event]?.decrypt(event) } class PrivateZapDecryptCache( signer: NostrSigner, -) : DecryptCache<LnZapRequestEvent, LnZapPrivateEvent>(signer) { +) : DecryptCache<ZapRequestEvent, LnZapPrivateEvent>(signer) { override suspend fun decryptAndParse( - event: LnZapRequestEvent, + event: ZapRequestEvent, signer: NostrSigner, ): LnZapPrivateEvent = signer.decryptZapEvent(event) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt index 2b91ea6934..595dc24600 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt @@ -73,7 +73,7 @@ class PrivateZapRequestBuilder { } fun decryptZapEvent( - event: LnZapRequestEvent, + event: ZapRequestEvent, signer: NostrSignerSync, ): LnZapPrivateEvent { if (signer.keyPair.privKey == null) throw SignerExceptions.ReadOnlyException() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/ZapReceiptEvent.kt similarity index 87% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/ZapReceiptEvent.kt index 5bbf5d889e..c8df2c63a9 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/ZapReceiptEvent.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.utils.Log @Immutable -class LnZapEvent( +class ZapReceiptEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -44,7 +44,7 @@ class LnZapEvent( content: String, sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig), - LnZapEventInterface, + ZapReceiptEventInterface, EventHintProvider, AddressHintProvider, PubKeyHintProvider, @@ -75,23 +75,23 @@ class LnZapEvent( // This event is also kept in LocalCache (same object) @kotlinx.serialization.Transient @kotlin.jvm.Transient - val zapRequest: LnZapRequestEvent? + val zapRequest: ZapRequestEvent? // Keeps this as a field because it's a heavier function used everywhere. val amount by lazy { try { lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) } } catch (e: Exception) { - Log.e("LnZapEvent", "Failed to Parse LnInvoice ${lnInvoice()}", e) + Log.e("ZapReceiptEvent", "Failed to Parse LnInvoice ${lnInvoice()}", e) null } } - override fun containedPost(): LnZapRequestEvent? = + override fun containedPost(): ZapRequestEvent? = try { - description()?.ifBlank { null }?.let { fromJson(it) } as? LnZapRequestEvent + description()?.ifBlank { null }?.let { fromJson(it) } as? ZapRequestEvent } catch (e: Exception) { - Log.w("LnZapEvent", "Failed to Parse Contained Post ${description()} in event $id", e) + Log.w("ZapReceiptEvent", "Failed to Parse Contained Post ${description()} in event $id", e) null } @@ -111,7 +111,7 @@ class LnZapEvent( ?.get(1) ?.toInt() } catch (e: Exception) { - Log.e("LnZapEvent", "ZappedPollOption failed to parse", e) + Log.e("ZapReceiptEvent", "ZappedPollOption failed to parse", e) null } @@ -135,3 +135,9 @@ class LnZapEvent( NONZAP, } } + +@Deprecated( + "Renamed to ZapReceiptEvent. NIP-57 names kind 9735 the zap receipt.", + ReplaceWith("ZapReceiptEvent", "com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent"), +) +typealias LnZapEvent = ZapReceiptEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapEventInterface.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/ZapReceiptEventInterface.kt similarity index 82% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapEventInterface.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/ZapReceiptEventInterface.kt index d5fc8c0d96..5a188986a1 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapEventInterface.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/ZapReceiptEventInterface.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.utils.BigDecimal @Immutable -interface LnZapEventInterface { +interface ZapReceiptEventInterface { fun zappedPost(): List<String> fun zappedPollOption(): Int? @@ -38,3 +38,9 @@ interface LnZapEventInterface { fun containedPost(): Event? } + +@Deprecated( + "Renamed to ZapReceiptEventInterface. Follows the LnZapEvent to ZapReceiptEvent rename (NIP-57 zap receipt).", + ReplaceWith("ZapReceiptEventInterface", "com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEventInterface"), +) +typealias LnZapEventInterface = ZapReceiptEventInterface diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/ZapRequestEvent.kt similarity index 91% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/ZapRequestEvent.kt index 7347f6bf62..7896537350 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/ZapRequestEvent.kt @@ -43,7 +43,7 @@ import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class LnZapRequestEvent( +class ZapRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -113,12 +113,12 @@ class LnZapRequestEvent( signer: NostrSigner, pollOption: Int?, message: String, - zapType: LnZapEvent.ZapType, + zapType: ZapReceiptEvent.ZapType, toUserPubHex: String?, amountMillisats: Long? = null, lnurl: String? = null, createdAt: Long = TimeUtils.now(), - ): LnZapRequestEvent { + ): ZapRequestEvent { var tags = listOf( arrayOf("e", zappedEvent.id), @@ -146,21 +146,21 @@ class LnZapRequestEvent( } return when (zapType) { - LnZapEvent.ZapType.PUBLIC -> { + ZapReceiptEvent.ZapType.PUBLIC -> { signer.sign(createdAt, KIND, tags.toTypedArray(), message) } - LnZapEvent.ZapType.ANONYMOUS -> { + ZapReceiptEvent.ZapType.ANONYMOUS -> { tags = tags + listOf(arrayOf("anon")) NostrSignerInternal(KeyPair()).sign(createdAt, KIND, tags.toTypedArray(), message) } - LnZapEvent.ZapType.PRIVATE -> { + ZapReceiptEvent.ZapType.PRIVATE -> { tags = tags + listOf(arrayOf("anon", "")) signer.sign(createdAt, KIND, tags.toTypedArray(), message) } - LnZapEvent.ZapType.NONZAP -> { + ZapReceiptEvent.ZapType.NONZAP -> { throw IllegalArgumentException("Invalid zap type") } } @@ -171,11 +171,11 @@ class LnZapRequestEvent( relays: Set<NormalizedRelayUrl>, signer: NostrSigner, message: String, - zapType: LnZapEvent.ZapType, + zapType: ZapReceiptEvent.ZapType, amountMillisats: Long? = null, lnurl: String? = null, createdAt: Long = TimeUtils.now(), - ): LnZapRequestEvent { + ): ZapRequestEvent { var tags = arrayOf( arrayOf("p", userHex), @@ -189,11 +189,11 @@ class LnZapRequestEvent( } return when (zapType) { - LnZapEvent.ZapType.PUBLIC -> { + ZapReceiptEvent.ZapType.PUBLIC -> { signer.sign(createdAt, KIND, tags, message) } - LnZapEvent.ZapType.ANONYMOUS -> { + ZapReceiptEvent.ZapType.ANONYMOUS -> { // Valueless `anon` tag: a blank-valued one (`["anon", ""]`) is the // marker for an *unsigned private* zap and would make the throwaway // signer encrypt the message instead of keeping it public. @@ -201,15 +201,21 @@ class LnZapRequestEvent( NostrSignerInternal(KeyPair()).sign(createdAt, KIND, tags, message) } - LnZapEvent.ZapType.PRIVATE -> { + ZapReceiptEvent.ZapType.PRIVATE -> { tags += arrayOf(arrayOf("anon", "")) signer.sign(createdAt, KIND, tags, message) } - LnZapEvent.ZapType.NONZAP -> { + ZapReceiptEvent.ZapType.NONZAP -> { throw IllegalArgumentException("Invalid zap type") } } } } } + +@Deprecated( + "Renamed to ZapRequestEvent. NIP-57 names kind 9734 the zap request.", + ReplaceWith("ZapRequestEvent", "com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent"), +) +typealias LnZapRequestEvent = ZapRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/LnZapReceiptValidator.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/LnZapReceiptValidator.kt index c6206814f5..da42cdb131 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/LnZapReceiptValidator.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/LnZapReceiptValidator.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.quartz.nip57Zaps.validate import com.vitorpamplona.quartz.lightning.LnInvoiceUtil import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.utils.BigDecimal /** @@ -84,7 +84,7 @@ object LnZapReceiptValidator { * When true, a missing `amount` tag is treated as a failure. */ fun validate( - receipt: LnZapEvent, + receipt: ZapReceiptEvent, expectedNostrPubkey: HexKey?, expectedLnurl: String?, strictAmount: Boolean = false, @@ -141,13 +141,13 @@ object LnZapReceiptValidator { return Result.Valid } - private fun LnZapRequestEvent.amountMillisats(): Long? = + private fun ZapRequestEvent.amountMillisats(): Long? = tags .firstOrNull { it.size > 1 && it[0] == "amount" } ?.get(1) ?.toLongOrNull() - private fun LnZapRequestEvent.lnurl(): String? = + private fun ZapRequestEvent.lnurl(): String? = tags .firstOrNull { it.size > 1 && it[0] == "lnurl" } ?.get(1) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/HasInnerEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/HasInnerEvent.kt index 45b0b0af55..1e948258c7 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/HasInnerEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/HasInnerEvent.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey /** * Marker for NIP-59 wrappers that carry an inner-event id once decrypted — - * GiftWrapEvent (and its subclasses) and SealedRumorEvent. Lets callers walk + * GiftWrapEvent (and its subclasses) and SealEvent. Lets callers walk * to the wrapped event without branching on the concrete wrapper type. */ interface HasInnerEvent { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/rumors/Rumor.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/rumors/Rumor.kt index c1ca788297..6d3d79d0b8 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/rumors/Rumor.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/rumors/Rumor.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher import com.vitorpamplona.quartz.nip59Giftwrap.rumors.kotlinSerialization.RumorKSerializer -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent import com.vitorpamplona.quartz.utils.EventFactory import kotlinx.serialization.Serializable @@ -41,7 +41,7 @@ class Rumor( val tags: Array<Array<String>>?, val content: String?, ) : OptimizedSerializable { - fun mergeWith(event: SealedRumorEvent): Event { + fun mergeWith(event: SealEvent): Event { val newPubKey = event.pubKey // forces to be the pubkey of the seal to make sure impersonators don't impersonate val newCreatedAt = if (createdAt != null && createdAt > 1000) createdAt else event.createdAt val newKind = kind ?: -1 diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealEvent.kt similarity index 93% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealEvent.kt index d37a771984..badc0cbc7d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealEvent.kt @@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.concurrent.Volatile @Immutable -class SealedRumorEvent( +class SealEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -48,9 +48,9 @@ class SealedRumorEvent( @Volatile override var innerEventId: HexKey? = null - fun copyNoContent(): SealedRumorEvent { + fun copyNoContent(): SealEvent { val copy = - SealedRumorEvent( + SealEvent( id, pubKey, createdAt, @@ -98,7 +98,7 @@ class SealedRumorEvent( signer: NostrSigner, expirationDelta: Long? = null, createdAt: Long = TimeUtils.now(), - ): SealedRumorEvent { + ): SealEvent { val rumor = Rumor.create(event) return create(rumor, encryptTo, signer, expirationDelta, createdAt) } @@ -109,7 +109,7 @@ class SealedRumorEvent( signer: NostrSigner, expirationDelta: Long? = null, createdAt: Long = TimeUtils.randomWithTwoDays(), - ): SealedRumorEvent { + ): SealEvent { val msg = Rumor.toJson(rumor) val tags = @@ -128,3 +128,9 @@ class SealedRumorEvent( } } } + +@Deprecated( + "Renamed to SealEvent. NIP-59 names kind 13 the seal.", + ReplaceWith("SealEvent", "com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent"), +) +typealias SealedRumorEvent = SealEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip60Cashu/history/CashuSpendingHistoryEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip60Cashu/history/CashuSpendingHistoryEvent.kt index adcd455875..66cf469bf6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip60Cashu/history/CashuSpendingHistoryEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip60Cashu/history/CashuSpendingHistoryEvent.kt @@ -25,10 +25,15 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent +import com.vitorpamplona.quartz.nip61Nutzaps.redemption.redeemedNutzaps import com.vitorpamplona.quartz.utils.TimeUtils /** @@ -43,6 +48,10 @@ import com.vitorpamplona.quartz.utils.TimeUtils * * The "e" tags with "redeemed" marker SHOULD be left unencrypted in the tags array. * All other "e" tags should be encrypted in the content. + * + * NIP-61 nutzap redemptions are this same kind: a history entry whose public tags + * carry the redeemed nutzap (`["e", <nutzap-id>, <relay>, "redeemed"]`) and the + * sender's `p` tag. See `buildNutzapRedemption` in `nip61Nutzaps.redemption`. */ @Immutable class CashuSpendingHistoryEvent( @@ -52,7 +61,22 @@ class CashuSpendingHistoryEvent( tags: Array<Array<String>>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { +) : Event(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + PubKeyHintProvider { + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + + override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + + /** + * NIP-61: the nutzaps this entry redeemed, from the public `e` tags marked `redeemed`. + */ + fun redeemedNutzaps() = tags.redeemedNutzaps() + /** * Decrypts the content to get the private spending history tags. */ diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/redemption/NutzapRedemptionEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/redemption/NutzapRedemptionEvent.kt index 8cfc908eef..1382f57b12 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/redemption/NutzapRedemptionEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/redemption/NutzapRedemptionEvent.kt @@ -20,52 +20,37 @@ */ package com.vitorpamplona.quartz.nip61Nutzaps.redemption -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle -import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider -import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip60Cashu.history.CashuSpendingHistoryEvent import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent import com.vitorpamplona.quartz.utils.TimeUtils -@Immutable -class NutzapRedemptionEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array<Array<String>>, - content: String, - sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig), - EventHintProvider, - PubKeyHintProvider { - override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) - - override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) - - override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) - - override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) - - fun redeemedNutzaps() = tags.redeemedNutzaps() - - companion object { - const val KIND = 7376 - - fun build( - nutzap: EventHintBundle<NutzapEvent>, - encryptedContent: String, - createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NutzapRedemptionEvent>.() -> Unit = {}, - ) = eventTemplate(KIND, encryptedContent, createdAt) { - redeemedNutzap(nutzap) - notifySender(nutzap) - initializer() - } - } +/** + * Builds a NIP-61 nutzap redemption: a NIP-60 spending history event (kind 7376) + * that publicly marks [nutzap] as redeemed and notifies its sender. + * + * [encryptedContent] is the NIP-44 encrypted history payload (direction, amount, ...). + */ +fun CashuSpendingHistoryEvent.Companion.buildNutzapRedemption( + nutzap: EventHintBundle<NutzapEvent>, + encryptedContent: String, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder<CashuSpendingHistoryEvent>.() -> Unit = {}, +) = eventTemplate(KIND, encryptedContent, createdAt) { + redeemedNutzap(nutzap) + notifySender(nutzap) + initializer() } + +/** + * NIP-61 redemptions are NIP-60 spending history events (kind 7376). This used to be a + * second class for the same kind, which `EventFactory` could never instantiate because + * [CashuSpendingHistoryEvent] matched first. + */ +@Deprecated( + "Kind 7376 is the NIP-60 spending history event. Use CashuSpendingHistoryEvent and CashuSpendingHistoryEvent.buildNutzapRedemption.", + ReplaceWith("CashuSpendingHistoryEvent", "com.vitorpamplona.quartz.nip60Cashu.history.CashuSpendingHistoryEvent"), +) +typealias NutzapRedemptionEvent = CashuSpendingHistoryEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/redemption/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/redemption/TagArrayBuilderExt.kt index 26ffc67ab9..cfd334df4e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/redemption/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/redemption/TagArrayBuilderExt.kt @@ -23,10 +23,11 @@ package com.vitorpamplona.quartz.nip61Nutzaps.redemption import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip60Cashu.history.CashuSpendingHistoryEvent import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent import com.vitorpamplona.quartz.utils.arrayOfNotNull -fun TagArrayBuilder<NutzapRedemptionEvent>.redeemedNutzap(nutzap: EventHintBundle<NutzapEvent>) = +fun TagArrayBuilder<CashuSpendingHistoryEvent>.redeemedNutzap(nutzap: EventHintBundle<NutzapEvent>) = add( arrayOfNotNull( "e", @@ -36,4 +37,4 @@ fun TagArrayBuilder<NutzapRedemptionEvent>.redeemedNutzap(nutzap: EventHintBundl ), ) -fun TagArrayBuilder<NutzapRedemptionEvent>.notifySender(nutzap: EventHintBundle<NutzapEvent>) = add(PTag.assemble(nutzap.event.pubKey, nutzap.authorHomeRelay)) +fun TagArrayBuilder<CashuSpendingHistoryEvent>.notifySender(nutzap: EventHintBundle<NutzapEvent>) = add(PTag.assemble(nutzap.event.pubKey, nutzap.authorHomeRelay)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/token/TokenEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/token/TokenEvent.kt index 0970ccde4a..0e72608c50 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/token/TokenEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/token/TokenEvent.kt @@ -20,31 +20,15 @@ */ package com.vitorpamplona.quartz.nip61Nutzaps.token -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder -import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate -import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenEvent -@Immutable -class TokenEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array<Array<String>>, - content: String, - sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { - companion object { - const val KIND = 7375 - - fun build( - encryptedContent: String, - createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<TokenEvent>.() -> Unit = {}, - ) = eventTemplate(KIND, encryptedContent, createdAt) { - initializer() - } - } -} +/** + * NIP-61 does not define its own kind 7375: redeemed nutzap proofs are stored as + * ordinary NIP-60 wallet token events. This used to be a second class for the same + * kind, which `EventFactory` could never instantiate because [CashuTokenEvent] matched first. + */ +@Deprecated( + "Kind 7375 is the NIP-60 Cashu wallet token event. Use CashuTokenEvent.", + ReplaceWith("CashuTokenEvent", "com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenEvent"), +) +typealias TokenEvent = CashuTokenEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/VideoHorizontalEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/AddressableNormalVideoEvent.kt similarity index 82% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/VideoHorizontalEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/AddressableNormalVideoEvent.kt index 67abb889ff..9f979d38f0 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/VideoHorizontalEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/AddressableNormalVideoEvent.kt @@ -32,7 +32,7 @@ import kotlin.uuid.Uuid @Immutable @OptIn(ExperimentalUuidApi::class) -class VideoHorizontalEvent( +class AddressableNormalVideoEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -49,7 +49,7 @@ class VideoHorizontalEvent( description: String, dTag: String = Uuid.random().toString(), createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<VideoHorizontalEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<AddressableNormalVideoEvent>.() -> Unit = {}, ) = build(description, dTag, createdAt) { videoIMeta(video) initializer() @@ -60,7 +60,7 @@ class VideoHorizontalEvent( description: String, dTag: String = Uuid.random().toString(), createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<VideoHorizontalEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<AddressableNormalVideoEvent>.() -> Unit = {}, ) = build(description, dTag, createdAt) { videoIMetas(video) initializer() @@ -70,10 +70,16 @@ class VideoHorizontalEvent( description: String, dTag: String = Uuid.random().toString(), createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<VideoHorizontalEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<AddressableNormalVideoEvent>.() -> Unit = {}, ) = eventTemplate(KIND, description, createdAt) { dTag(dTag) initializer() } } } + +@Deprecated( + "Renamed to AddressableNormalVideoEvent. NIP-71 names kind 34235 the addressable normal video.", + ReplaceWith("AddressableNormalVideoEvent", "com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent"), +) +typealias VideoHorizontalEvent = AddressableNormalVideoEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/VideoVerticalEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/AddressableShortVideoEvent.kt similarity index 83% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/VideoVerticalEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/AddressableShortVideoEvent.kt index 880c8bf885..0c0347707d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/VideoVerticalEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/AddressableShortVideoEvent.kt @@ -32,7 +32,7 @@ import kotlin.uuid.Uuid @Immutable @OptIn(ExperimentalUuidApi::class) -class VideoVerticalEvent( +class AddressableShortVideoEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -49,7 +49,7 @@ class VideoVerticalEvent( description: String, dTag: String = Uuid.random().toString(), createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<VideoVerticalEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<AddressableShortVideoEvent>.() -> Unit = {}, ) = build(description, dTag, createdAt) { videoIMeta(video) initializer() @@ -59,10 +59,16 @@ class VideoVerticalEvent( description: String, dTag: String = Uuid.random().toString(), createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<VideoVerticalEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<AddressableShortVideoEvent>.() -> Unit = {}, ) = eventTemplate(KIND, description, createdAt) { dTag(dTag) initializer() } } } + +@Deprecated( + "Renamed to AddressableShortVideoEvent. NIP-71 names kind 34236 the addressable short video.", + ReplaceWith("AddressableShortVideoEvent", "com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent"), +) +typealias VideoVerticalEvent = AddressableShortVideoEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip75ZapGoals/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip75ZapGoals/TagArrayBuilderExt.kt index 117e00a207..3bde9fbdd1 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip75ZapGoals/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip75ZapGoals/TagArrayBuilderExt.kt @@ -30,16 +30,16 @@ import com.vitorpamplona.quartz.nip75ZapGoals.tags.AmountTag import com.vitorpamplona.quartz.nip75ZapGoals.tags.ClosedAtTag import com.vitorpamplona.quartz.nip75ZapGoals.tags.RelayListTag -fun TagArrayBuilder<GoalEvent>.amount(amountInMillisats: Long) = addUnique(AmountTag.assemble(amountInMillisats)) +fun TagArrayBuilder<ZapGoalEvent>.amount(amountInMillisats: Long) = addUnique(AmountTag.assemble(amountInMillisats)) -fun TagArrayBuilder<GoalEvent>.relays(urls: List<NormalizedRelayUrl>) = add(RelayListTag.assemble(urls)) +fun TagArrayBuilder<ZapGoalEvent>.relays(urls: List<NormalizedRelayUrl>) = add(RelayListTag.assemble(urls)) -fun TagArrayBuilder<GoalEvent>.summary(summary: String) = addUnique(SummaryTag.assemble(summary)) +fun TagArrayBuilder<ZapGoalEvent>.summary(summary: String) = addUnique(SummaryTag.assemble(summary)) -fun TagArrayBuilder<GoalEvent>.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) +fun TagArrayBuilder<ZapGoalEvent>.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) -fun TagArrayBuilder<GoalEvent>.closedAt(closedAt: Long) = addUnique(ClosedAtTag.assemble(closedAt)) +fun TagArrayBuilder<ZapGoalEvent>.closedAt(closedAt: Long) = addUnique(ClosedAtTag.assemble(closedAt)) -fun TagArrayBuilder<GoalEvent>.linked(tag: ETag) = addUnique(tag.toTagArray()) +fun TagArrayBuilder<ZapGoalEvent>.linked(tag: ETag) = addUnique(tag.toTagArray()) -fun TagArrayBuilder<GoalEvent>.linked(tag: ATag) = addUnique(tag.toATagArray()) +fun TagArrayBuilder<ZapGoalEvent>.linked(tag: ATag) = addUnique(tag.toATagArray()) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip75ZapGoals/ZapGoalEvent.kt similarity index 94% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip75ZapGoals/ZapGoalEvent.kt index da7b923c80..66f3007031 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip75ZapGoals/ZapGoalEvent.kt @@ -48,7 +48,7 @@ import com.vitorpamplona.quartz.nip75ZapGoals.tags.RelayListTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class GoalEvent( +class ZapGoalEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -106,7 +106,7 @@ class GoalEvent( websiteUrl: String? = null, linkedEvent: EventHintBundle<Event>? = null, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<GoalEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<ZapGoalEvent>.() -> Unit = {}, ) = eventTemplate(KIND, description, createdAt) { amount(amount) relays(relays) @@ -124,3 +124,9 @@ class GoalEvent( } } } + +@Deprecated( + "Renamed to ZapGoalEvent. NIP-75 names kind 9041 the zap goal.", + ReplaceWith("ZapGoalEvent", "com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent"), +) +typealias GoalEvent = ZapGoalEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip85TrustedAssertions/users/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip85TrustedAssertions/users/TagArrayBuilderExt.kt index 161912e25d..1cdb5412b7 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip85TrustedAssertions/users/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip85TrustedAssertions/users/TagArrayBuilderExt.kt @@ -42,42 +42,42 @@ import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ZapAvgAmountDa import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ZapCountReceivedTag import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ZapCountSentTag -fun TagArrayBuilder<ContactCardEvent>.rank(rank: Int) = addUnique(RankTag.assemble(rank)) +fun TagArrayBuilder<UserAssertionEvent>.rank(rank: Int) = addUnique(RankTag.assemble(rank)) -fun TagArrayBuilder<ContactCardEvent>.followers(count: Int) = addUnique(FollowerCountTag.assemble(count)) +fun TagArrayBuilder<UserAssertionEvent>.followers(count: Int) = addUnique(FollowerCountTag.assemble(count)) -fun TagArrayBuilder<ContactCardEvent>.hops(hops: Int) = addUnique(HopsTag.assemble(hops)) +fun TagArrayBuilder<UserAssertionEvent>.hops(hops: Int) = addUnique(HopsTag.assemble(hops)) -fun TagArrayBuilder<ContactCardEvent>.firstCreatedAt(timestamp: Long) = addUnique(FirstCreatedAtTag.assemble(timestamp)) +fun TagArrayBuilder<UserAssertionEvent>.firstCreatedAt(timestamp: Long) = addUnique(FirstCreatedAtTag.assemble(timestamp)) -fun TagArrayBuilder<ContactCardEvent>.postCount(count: Int) = addUnique(PostCountTag.assemble(count)) +fun TagArrayBuilder<UserAssertionEvent>.postCount(count: Int) = addUnique(PostCountTag.assemble(count)) -fun TagArrayBuilder<ContactCardEvent>.replyCount(count: Int) = addUnique(ReplyCountTag.assemble(count)) +fun TagArrayBuilder<UserAssertionEvent>.replyCount(count: Int) = addUnique(ReplyCountTag.assemble(count)) -fun TagArrayBuilder<ContactCardEvent>.reactionsCount(count: Int) = addUnique(ReactionsCountTag.assemble(count)) +fun TagArrayBuilder<UserAssertionEvent>.reactionsCount(count: Int) = addUnique(ReactionsCountTag.assemble(count)) -fun TagArrayBuilder<ContactCardEvent>.zapAmountReceived(sats: Long) = addUnique(ZapAmountReceivedTag.assemble(sats)) +fun TagArrayBuilder<UserAssertionEvent>.zapAmountReceived(sats: Long) = addUnique(ZapAmountReceivedTag.assemble(sats)) -fun TagArrayBuilder<ContactCardEvent>.zapAmountSent(sats: Long) = addUnique(ZapAmountSentTag.assemble(sats)) +fun TagArrayBuilder<UserAssertionEvent>.zapAmountSent(sats: Long) = addUnique(ZapAmountSentTag.assemble(sats)) -fun TagArrayBuilder<ContactCardEvent>.zapCountReceived(count: Int) = addUnique(ZapCountReceivedTag.assemble(count)) +fun TagArrayBuilder<UserAssertionEvent>.zapCountReceived(count: Int) = addUnique(ZapCountReceivedTag.assemble(count)) -fun TagArrayBuilder<ContactCardEvent>.zapCountSent(count: Int) = addUnique(ZapCountSentTag.assemble(count)) +fun TagArrayBuilder<UserAssertionEvent>.zapCountSent(count: Int) = addUnique(ZapCountSentTag.assemble(count)) -fun TagArrayBuilder<ContactCardEvent>.zapAvgAmountDayReceived(sats: Long) = addUnique(ZapAvgAmountDayReceivedTag.assemble(sats)) +fun TagArrayBuilder<UserAssertionEvent>.zapAvgAmountDayReceived(sats: Long) = addUnique(ZapAvgAmountDayReceivedTag.assemble(sats)) -fun TagArrayBuilder<ContactCardEvent>.zapAvgAmountDaySent(sats: Long) = addUnique(ZapAvgAmountDaySentTag.assemble(sats)) +fun TagArrayBuilder<UserAssertionEvent>.zapAvgAmountDaySent(sats: Long) = addUnique(ZapAvgAmountDaySentTag.assemble(sats)) -fun TagArrayBuilder<ContactCardEvent>.reportsCountReceived(count: Int) = addUnique(ReportsCountReceivedTag.assemble(count)) +fun TagArrayBuilder<UserAssertionEvent>.reportsCountReceived(count: Int) = addUnique(ReportsCountReceivedTag.assemble(count)) -fun TagArrayBuilder<ContactCardEvent>.reportsCountSent(count: Int) = addUnique(ReportsCountSentTag.assemble(count)) +fun TagArrayBuilder<UserAssertionEvent>.reportsCountSent(count: Int) = addUnique(ReportsCountSentTag.assemble(count)) -fun TagArrayBuilder<ContactCardEvent>.topic(topic: String) = add(TopicTag.assemble(topic)) +fun TagArrayBuilder<UserAssertionEvent>.topic(topic: String) = add(TopicTag.assemble(topic)) -fun TagArrayBuilder<ContactCardEvent>.activeHoursStart(hour: Int) = addUnique(ActiveHoursStartTag.assemble(hour)) +fun TagArrayBuilder<UserAssertionEvent>.activeHoursStart(hour: Int) = addUnique(ActiveHoursStartTag.assemble(hour)) -fun TagArrayBuilder<ContactCardEvent>.activeHoursEnd(hour: Int) = addUnique(ActiveHoursEndTag.assemble(hour)) +fun TagArrayBuilder<UserAssertionEvent>.activeHoursEnd(hour: Int) = addUnique(ActiveHoursEndTag.assemble(hour)) -fun TagArrayBuilder<ContactCardEvent>.petName(name: String) = addUnique(PetNameTag.assemble(name)) +fun TagArrayBuilder<UserAssertionEvent>.petName(name: String) = addUnique(PetNameTag.assemble(name)) -fun TagArrayBuilder<ContactCardEvent>.summary(summary: String) = addUnique(SummaryTag.assemble(summary)) +fun TagArrayBuilder<UserAssertionEvent>.summary(summary: String) = addUnique(SummaryTag.assemble(summary)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip85TrustedAssertions/users/ContactCardEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip85TrustedAssertions/users/UserAssertionEvent.kt similarity index 89% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip85TrustedAssertions/users/ContactCardEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip85TrustedAssertions/users/UserAssertionEvent.kt index 91e7d58355..055df00154 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip85TrustedAssertions/users/ContactCardEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip85TrustedAssertions/users/UserAssertionEvent.kt @@ -43,7 +43,7 @@ import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.SummaryTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class ContactCardEvent( +class UserAssertionEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -127,9 +127,9 @@ class ContactCardEvent( emojis: List<EmojiUrlTag> = emptyList(), signer: NostrSigner, createdAt: Long = TimeUtils.now(), - publicInitializer: TagArrayBuilder<ContactCardEvent>.() -> Unit = {}, - privateInitializer: TagArrayBuilder<ContactCardEvent>.() -> Unit = {}, - ): ContactCardEvent = signer.sign(build(targetUser, petName, summary, emojis, signer, createdAt, publicInitializer, privateInitializer)) + publicInitializer: TagArrayBuilder<UserAssertionEvent>.() -> Unit = {}, + privateInitializer: TagArrayBuilder<UserAssertionEvent>.() -> Unit = {}, + ): UserAssertionEvent = signer.sign(build(targetUser, petName, summary, emojis, signer, createdAt, publicInitializer, privateInitializer)) /** * Unsigned template for a new card about [targetUser]. The petname, summary @@ -143,9 +143,9 @@ class ContactCardEvent( emojis: List<EmojiUrlTag> = emptyList(), signer: NostrSigner, createdAt: Long = TimeUtils.now(), - publicInitializer: TagArrayBuilder<ContactCardEvent>.() -> Unit = {}, - privateInitializer: TagArrayBuilder<ContactCardEvent>.() -> Unit = {}, - ): EventTemplate<ContactCardEvent> { + publicInitializer: TagArrayBuilder<UserAssertionEvent>.() -> Unit = {}, + privateInitializer: TagArrayBuilder<UserAssertionEvent>.() -> Unit = {}, + ): EventTemplate<UserAssertionEvent> { val privateTags = tagArray { petName?.let { petName(it) } @@ -174,13 +174,13 @@ class ContactCardEvent( * [signer] only decrypts/encrypts here; the caller signs the template. */ suspend fun updatePetNameAndSummary( - earlierVersion: ContactCardEvent, + earlierVersion: UserAssertionEvent, petName: String? = null, summary: String? = null, emojis: List<EmojiUrlTag> = emptyList(), signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): EventTemplate<ContactCardEvent> { + ): EventTemplate<UserAssertionEvent> { val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() @@ -211,3 +211,9 @@ class ContactCardEvent( } } } + +@Deprecated( + "Renamed to UserAssertionEvent. NIP-85 names kind 30382 the user trusted assertion.", + ReplaceWith("UserAssertionEvent", "com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent"), +) +typealias ContactCardEvent = UserAssertionEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/NostrSignerWithClientTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/NostrSignerWithClientTag.kt index 7cd234f925..48c72d8de2 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/NostrSignerWithClientTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/NostrSignerWithClientTag.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent /** * A [NostrSigner] decorator that automatically appends a NIP-89 client tag @@ -94,7 +94,7 @@ class NostrSignerWithClientTag( fromPublicKey: HexKey, ): String = inner.nip44Decrypt(ciphertext, fromPublicKey) - override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent = inner.decryptZapEvent(event) + override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = inner.decryptZapEvent(event) override suspend fun deriveKey(nonce: HexKey): HexKey = inner.deriveKey(nonce) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentDiscoveryRequest/NIP90ContentDiscoveryRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentDiscoveryRequest/DvmContentDiscoveryRequestEvent.kt similarity index 85% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentDiscoveryRequest/NIP90ContentDiscoveryRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentDiscoveryRequest/DvmContentDiscoveryRequestEvent.kt index d6df3c9589..d7b0825f50 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentDiscoveryRequest/NIP90ContentDiscoveryRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentDiscoveryRequest/DvmContentDiscoveryRequestEvent.kt @@ -34,7 +34,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils @Stable @Immutable -class NIP90ContentDiscoveryRequestEvent( +class DvmContentDiscoveryRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -62,7 +62,7 @@ class NIP90ContentDiscoveryRequestEvent( forUser: HexKey, relays: Set<NormalizedRelayUrl>, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90ContentDiscoveryRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmContentDiscoveryRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { dvmPubKey(dvmPublicKey) relays(relays) @@ -72,3 +72,9 @@ class NIP90ContentDiscoveryRequestEvent( } } } + +@Deprecated( + "Renamed to DvmContentDiscoveryRequestEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmContentDiscoveryRequestEvent", "com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.DvmContentDiscoveryRequestEvent"), +) +typealias NIP90ContentDiscoveryRequestEvent = DvmContentDiscoveryRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentDiscoveryRequest/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentDiscoveryRequest/TagArrayBuilderExt.kt index b9ca5e65de..0af5322d05 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentDiscoveryRequest/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentDiscoveryRequest/TagArrayBuilderExt.kt @@ -26,11 +26,11 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.tags.ParamTag import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.tags.RelaysTag -fun TagArrayBuilder<NIP90ContentDiscoveryRequestEvent>.dvmPubKey(pubKey: HexKey) = addUnique(arrayOf("p", pubKey)) +fun TagArrayBuilder<DvmContentDiscoveryRequestEvent>.dvmPubKey(pubKey: HexKey) = addUnique(arrayOf("p", pubKey)) -fun TagArrayBuilder<NIP90ContentDiscoveryRequestEvent>.relays(relays: Set<NormalizedRelayUrl>) = addUnique(RelaysTag.assemble(relays)) +fun TagArrayBuilder<DvmContentDiscoveryRequestEvent>.relays(relays: Set<NormalizedRelayUrl>) = addUnique(RelaysTag.assemble(relays)) -fun TagArrayBuilder<NIP90ContentDiscoveryRequestEvent>.param( +fun TagArrayBuilder<DvmContentDiscoveryRequestEvent>.param( key: String, value: String, ) = add(ParamTag.assemble(key, value)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentDiscoveryResponse/NIP90ContentDiscoveryResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentDiscoveryResponse/DvmContentDiscoveryResponseEvent.kt similarity index 82% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentDiscoveryResponse/NIP90ContentDiscoveryResponseEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentDiscoveryResponse/DvmContentDiscoveryResponseEvent.kt index 105a1d1fde..8b2cd681f7 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentDiscoveryResponse/NIP90ContentDiscoveryResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentDiscoveryResponse/DvmContentDiscoveryResponseEvent.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90ContentDiscoveryResponseEvent( +class DvmContentDiscoveryResponseEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -61,7 +61,7 @@ class NIP90ContentDiscoveryResponseEvent( } } } catch (e: Throwable) { - Log.w("NIP90ContentDiscoveryResponseEvent") { "Error parsing the JSON ${e.message}" } + Log.w("DvmContentDiscoveryResponseEvent") { "Error parsing the JSON ${e.message}" } } return events ?: listOf() @@ -72,9 +72,15 @@ class NIP90ContentDiscoveryResponseEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90ContentDiscoveryResponseEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmContentDiscoveryResponseEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmContentDiscoveryResponseEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmContentDiscoveryResponseEvent", "com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent"), +) +typealias NIP90ContentDiscoveryResponseEvent = DvmContentDiscoveryResponseEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentSearch/NIP90ContentSearchRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentSearch/DvmContentSearchRequestEvent.kt similarity index 89% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentSearch/NIP90ContentSearchRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentSearch/DvmContentSearchRequestEvent.kt index ca04176d38..910922bd11 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentSearch/NIP90ContentSearchRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentSearch/DvmContentSearchRequestEvent.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.quartz.nip90Dvms.tags.param import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90ContentSearchRequestEvent( +class DvmContentSearchRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -76,7 +76,7 @@ class NIP90ContentSearchRequestEvent( until: Long? = null, maxResults: Int? = null, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90ContentSearchRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmContentSearchRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { inputText(searchQuery) users?.let { param("users", it) } @@ -87,3 +87,9 @@ class NIP90ContentSearchRequestEvent( } } } + +@Deprecated( + "Renamed to DvmContentSearchRequestEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmContentSearchRequestEvent", "com.vitorpamplona.quartz.nip90Dvms.contentSearch.DvmContentSearchRequestEvent"), +) +typealias NIP90ContentSearchRequestEvent = DvmContentSearchRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentSearch/NIP90ContentSearchResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentSearch/DvmContentSearchResponseEvent.kt similarity index 83% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentSearch/NIP90ContentSearchResponseEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentSearch/DvmContentSearchResponseEvent.kt index 894a70dc90..013fa3abc7 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentSearch/NIP90ContentSearchResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/contentSearch/DvmContentSearchResponseEvent.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90ContentSearchResponseEvent( +class DvmContentSearchResponseEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -61,7 +61,7 @@ class NIP90ContentSearchResponseEvent( } } } catch (e: Throwable) { - Log.w("NIP90ContentSearchResponseEvent") { "Error parsing the JSON ${e.message}" } + Log.w("DvmContentSearchResponseEvent") { "Error parsing the JSON ${e.message}" } } return events ?: listOf() @@ -72,9 +72,15 @@ class NIP90ContentSearchResponseEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90ContentSearchResponseEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmContentSearchResponseEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmContentSearchResponseEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmContentSearchResponseEvent", "com.vitorpamplona.quartz.nip90Dvms.contentSearch.DvmContentSearchResponseEvent"), +) +typealias NIP90ContentSearchResponseEvent = DvmContentSearchResponseEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventCount/NIP90EventCountRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventCount/DvmEventCountRequestEvent.kt similarity index 86% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventCount/NIP90EventCountRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventCount/DvmEventCountRequestEvent.kt index 504672ea10..408c2be134 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventCount/NIP90EventCountRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventCount/DvmEventCountRequestEvent.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.quartz.nip90Dvms.tags.param import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90EventCountRequestEvent( +class DvmEventCountRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -58,7 +58,7 @@ class NIP90EventCountRequestEvent( groups: List<String> = emptyList(), filterJson: String = "", createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90EventCountRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmEventCountRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, filterJson, createdAt) { inputs.forEach { inputText(it) } relays.forEach { param("relay", it) } @@ -67,3 +67,9 @@ class NIP90EventCountRequestEvent( } } } + +@Deprecated( + "Renamed to DvmEventCountRequestEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmEventCountRequestEvent", "com.vitorpamplona.quartz.nip90Dvms.eventCount.DvmEventCountRequestEvent"), +) +typealias NIP90EventCountRequestEvent = DvmEventCountRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventCount/NIP90EventCountResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventCount/DvmEventCountResponseEvent.kt similarity index 82% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventCount/NIP90EventCountResponseEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventCount/DvmEventCountResponseEvent.kt index 84f0d173d6..10e0e62444 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventCount/NIP90EventCountResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventCount/DvmEventCountResponseEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90EventCountResponseEvent( +class DvmEventCountResponseEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -43,9 +43,15 @@ class NIP90EventCountResponseEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90EventCountResponseEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmEventCountResponseEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmEventCountResponseEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmEventCountResponseEvent", "com.vitorpamplona.quartz.nip90Dvms.eventCount.DvmEventCountResponseEvent"), +) +typealias NIP90EventCountResponseEvent = DvmEventCountResponseEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPowDelegation/NIP90EventPowDelegationRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPowDelegation/DvmEventPowDelegationRequestEvent.kt similarity index 83% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPowDelegation/NIP90EventPowDelegationRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPowDelegation/DvmEventPowDelegationRequestEvent.kt index 1f0d7570ae..ee577cb305 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPowDelegation/NIP90EventPowDelegationRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPowDelegation/DvmEventPowDelegationRequestEvent.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.quartz.nip90Dvms.tags.param import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90EventPowDelegationRequestEvent( +class DvmEventPowDelegationRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -54,7 +54,7 @@ class NIP90EventPowDelegationRequestEvent( eventJson: String, pow: Int, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90EventPowDelegationRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmEventPowDelegationRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { inputText(eventJson) param("pow", pow.toString()) @@ -62,3 +62,9 @@ class NIP90EventPowDelegationRequestEvent( } } } + +@Deprecated( + "Renamed to DvmEventPowDelegationRequestEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmEventPowDelegationRequestEvent", "com.vitorpamplona.quartz.nip90Dvms.eventPowDelegation.DvmEventPowDelegationRequestEvent"), +) +typealias NIP90EventPowDelegationRequestEvent = DvmEventPowDelegationRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPowDelegation/NIP90EventPowDelegationResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPowDelegation/DvmEventPowDelegationResponseEvent.kt similarity index 79% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPowDelegation/NIP90EventPowDelegationResponseEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPowDelegation/DvmEventPowDelegationResponseEvent.kt index ab46d67d28..0174d11e57 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPowDelegation/NIP90EventPowDelegationResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPowDelegation/DvmEventPowDelegationResponseEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90EventPowDelegationResponseEvent( +class DvmEventPowDelegationResponseEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -41,9 +41,15 @@ class NIP90EventPowDelegationResponseEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90EventPowDelegationResponseEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmEventPowDelegationResponseEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmEventPowDelegationResponseEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmEventPowDelegationResponseEvent", "com.vitorpamplona.quartz.nip90Dvms.eventPowDelegation.DvmEventPowDelegationResponseEvent"), +) +typealias NIP90EventPowDelegationResponseEvent = DvmEventPowDelegationResponseEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPublishSchedule/NIP90EventPublishScheduleRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPublishSchedule/DvmEventPublishScheduleRequestEvent.kt similarity index 84% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPublishSchedule/NIP90EventPublishScheduleRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPublishSchedule/DvmEventPublishScheduleRequestEvent.kt index a6eab69fa8..5160350b86 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPublishSchedule/NIP90EventPublishScheduleRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPublishSchedule/DvmEventPublishScheduleRequestEvent.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.quartz.nip90Dvms.tags.param import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90EventPublishScheduleRequestEvent( +class DvmEventPublishScheduleRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -54,7 +54,7 @@ class NIP90EventPublishScheduleRequestEvent( eventJson: String, relays: List<String> = emptyList(), createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90EventPublishScheduleRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmEventPublishScheduleRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { inputText(eventJson) if (relays.isNotEmpty()) { @@ -64,3 +64,9 @@ class NIP90EventPublishScheduleRequestEvent( } } } + +@Deprecated( + "Renamed to DvmEventPublishScheduleRequestEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmEventPublishScheduleRequestEvent", "com.vitorpamplona.quartz.nip90Dvms.eventPublishSchedule.DvmEventPublishScheduleRequestEvent"), +) +typealias NIP90EventPublishScheduleRequestEvent = DvmEventPublishScheduleRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPublishSchedule/NIP90EventPublishScheduleResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPublishSchedule/DvmEventPublishScheduleResponseEvent.kt similarity index 79% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPublishSchedule/NIP90EventPublishScheduleResponseEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPublishSchedule/DvmEventPublishScheduleResponseEvent.kt index 1dfa2e5980..8a5fde2e6e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPublishSchedule/NIP90EventPublishScheduleResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventPublishSchedule/DvmEventPublishScheduleResponseEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90EventPublishScheduleResponseEvent( +class DvmEventPublishScheduleResponseEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -43,9 +43,15 @@ class NIP90EventPublishScheduleResponseEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90EventPublishScheduleResponseEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmEventPublishScheduleResponseEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmEventPublishScheduleResponseEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmEventPublishScheduleResponseEvent", "com.vitorpamplona.quartz.nip90Dvms.eventPublishSchedule.DvmEventPublishScheduleResponseEvent"), +) +typealias NIP90EventPublishScheduleResponseEvent = DvmEventPublishScheduleResponseEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventTimestamping/NIP90EventTimestampingRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventTimestamping/DvmEventTimestampingRequestEvent.kt similarity index 83% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventTimestamping/NIP90EventTimestampingRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventTimestamping/DvmEventTimestampingRequestEvent.kt index 4280ea87ca..454e3574a2 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventTimestamping/NIP90EventTimestampingRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventTimestamping/DvmEventTimestampingRequestEvent.kt @@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.nip90Dvms.tags.inputs import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90EventTimestampingRequestEvent( +class DvmEventTimestampingRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -50,10 +50,16 @@ class NIP90EventTimestampingRequestEvent( fun build( eventId: HexKey, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90EventTimestampingRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmEventTimestampingRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { inputEvent(eventId) initializer() } } } + +@Deprecated( + "Renamed to DvmEventTimestampingRequestEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmEventTimestampingRequestEvent", "com.vitorpamplona.quartz.nip90Dvms.eventTimestamping.DvmEventTimestampingRequestEvent"), +) +typealias NIP90EventTimestampingRequestEvent = DvmEventTimestampingRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventTimestamping/NIP90EventTimestampingResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventTimestamping/DvmEventTimestampingResponseEvent.kt similarity index 80% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventTimestamping/NIP90EventTimestampingResponseEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventTimestamping/DvmEventTimestampingResponseEvent.kt index 604079190e..d4a1125fea 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventTimestamping/NIP90EventTimestampingResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/eventTimestamping/DvmEventTimestampingResponseEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90EventTimestampingResponseEvent( +class DvmEventTimestampingResponseEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -43,9 +43,15 @@ class NIP90EventTimestampingResponseEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90EventTimestampingResponseEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmEventTimestampingResponseEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmEventTimestampingResponseEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmEventTimestampingResponseEvent", "com.vitorpamplona.quartz.nip90Dvms.eventTimestamping.DvmEventTimestampingResponseEvent"), +) +typealias NIP90EventTimestampingResponseEvent = DvmEventTimestampingResponseEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageGeneration/NIP90ImageGenerationRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageGeneration/DvmImageGenerationRequestEvent.kt similarity index 89% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageGeneration/NIP90ImageGenerationRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageGeneration/DvmImageGenerationRequestEvent.kt index 08dc291512..7ffb56dad8 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageGeneration/NIP90ImageGenerationRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageGeneration/DvmImageGenerationRequestEvent.kt @@ -37,7 +37,7 @@ import com.vitorpamplona.quartz.nip90Dvms.tags.param import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90ImageGenerationRequestEvent( +class DvmImageGenerationRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -85,7 +85,7 @@ class NIP90ImageGenerationRequestEvent( size: String? = null, negativePrompt: String? = null, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90ImageGenerationRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmImageGenerationRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { sourceImageUrl?.let { inputUrl(it) } inputText(prompt) @@ -98,3 +98,9 @@ class NIP90ImageGenerationRequestEvent( } } } + +@Deprecated( + "Renamed to DvmImageGenerationRequestEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmImageGenerationRequestEvent", "com.vitorpamplona.quartz.nip90Dvms.imageGeneration.DvmImageGenerationRequestEvent"), +) +typealias NIP90ImageGenerationRequestEvent = DvmImageGenerationRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageGeneration/NIP90ImageGenerationResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageGeneration/DvmImageGenerationResponseEvent.kt similarity index 80% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageGeneration/NIP90ImageGenerationResponseEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageGeneration/DvmImageGenerationResponseEvent.kt index ef07dd5fee..7978dc4aab 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageGeneration/NIP90ImageGenerationResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageGeneration/DvmImageGenerationResponseEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90ImageGenerationResponseEvent( +class DvmImageGenerationResponseEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -41,9 +41,15 @@ class NIP90ImageGenerationResponseEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90ImageGenerationResponseEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmImageGenerationResponseEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmImageGenerationResponseEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmImageGenerationResponseEvent", "com.vitorpamplona.quartz.nip90Dvms.imageGeneration.DvmImageGenerationResponseEvent"), +) +typealias NIP90ImageGenerationResponseEvent = DvmImageGenerationResponseEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageToVideo/NIP90ImageToVideoRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageToVideo/DvmImageToVideoRequestEvent.kt similarity index 84% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageToVideo/NIP90ImageToVideoRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageToVideo/DvmImageToVideoRequestEvent.kt index 98557b94d5..a4d875fbaa 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageToVideo/NIP90ImageToVideoRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageToVideo/DvmImageToVideoRequestEvent.kt @@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.nip90Dvms.tags.inputs import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90ImageToVideoRequestEvent( +class DvmImageToVideoRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -50,10 +50,16 @@ class NIP90ImageToVideoRequestEvent( fun build( imageUrl: String, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90ImageToVideoRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmImageToVideoRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { inputUrl(imageUrl) initializer() } } } + +@Deprecated( + "Renamed to DvmImageToVideoRequestEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmImageToVideoRequestEvent", "com.vitorpamplona.quartz.nip90Dvms.imageToVideo.DvmImageToVideoRequestEvent"), +) +typealias NIP90ImageToVideoRequestEvent = DvmImageToVideoRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageToVideo/NIP90ImageToVideoResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageToVideo/DvmImageToVideoResponseEvent.kt similarity index 81% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageToVideo/NIP90ImageToVideoResponseEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageToVideo/DvmImageToVideoResponseEvent.kt index 9e3c21edfa..2fe0247505 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageToVideo/NIP90ImageToVideoResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/imageToVideo/DvmImageToVideoResponseEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90ImageToVideoResponseEvent( +class DvmImageToVideoResponseEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -41,9 +41,15 @@ class NIP90ImageToVideoResponseEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90ImageToVideoResponseEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmImageToVideoResponseEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmImageToVideoResponseEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmImageToVideoResponseEvent", "com.vitorpamplona.quartz.nip90Dvms.imageToVideo.DvmImageToVideoResponseEvent"), +) +typealias NIP90ImageToVideoResponseEvent = DvmImageToVideoResponseEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/malwareScanning/NIP90MalwareScanRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/malwareScanning/DvmMalwareScanRequestEvent.kt similarity index 84% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/malwareScanning/NIP90MalwareScanRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/malwareScanning/DvmMalwareScanRequestEvent.kt index af90743b08..9486fb274c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/malwareScanning/NIP90MalwareScanRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/malwareScanning/DvmMalwareScanRequestEvent.kt @@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.nip90Dvms.tags.inputs import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90MalwareScanRequestEvent( +class DvmMalwareScanRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -50,10 +50,16 @@ class NIP90MalwareScanRequestEvent( fun build( fileUrl: String, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90MalwareScanRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmMalwareScanRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { inputUrl(fileUrl) initializer() } } } + +@Deprecated( + "Renamed to DvmMalwareScanRequestEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmMalwareScanRequestEvent", "com.vitorpamplona.quartz.nip90Dvms.malwareScanning.DvmMalwareScanRequestEvent"), +) +typealias NIP90MalwareScanRequestEvent = DvmMalwareScanRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/malwareScanning/NIP90MalwareScanResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/malwareScanning/DvmMalwareScanResponseEvent.kt similarity index 81% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/malwareScanning/NIP90MalwareScanResponseEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/malwareScanning/DvmMalwareScanResponseEvent.kt index 37aae8e6df..d6cdace38d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/malwareScanning/NIP90MalwareScanResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/malwareScanning/DvmMalwareScanResponseEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90MalwareScanResponseEvent( +class DvmMalwareScanResponseEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -43,9 +43,15 @@ class NIP90MalwareScanResponseEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90MalwareScanResponseEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmMalwareScanResponseEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmMalwareScanResponseEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmMalwareScanResponseEvent", "com.vitorpamplona.quartz.nip90Dvms.malwareScanning.DvmMalwareScanResponseEvent"), +) +typealias NIP90MalwareScanResponseEvent = DvmMalwareScanResponseEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/opReturn/NIP90OpReturnRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/opReturn/DvmOpReturnRequestEvent.kt similarity index 85% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/opReturn/NIP90OpReturnRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/opReturn/DvmOpReturnRequestEvent.kt index 2cf9def354..478ab68e93 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/opReturn/NIP90OpReturnRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/opReturn/DvmOpReturnRequestEvent.kt @@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.nip90Dvms.tags.inputs import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90OpReturnRequestEvent( +class DvmOpReturnRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -51,10 +51,16 @@ class NIP90OpReturnRequestEvent( fun build( text: String, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90OpReturnRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmOpReturnRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { inputText(text) initializer() } } } + +@Deprecated( + "Renamed to DvmOpReturnRequestEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmOpReturnRequestEvent", "com.vitorpamplona.quartz.nip90Dvms.opReturn.DvmOpReturnRequestEvent"), +) +typealias NIP90OpReturnRequestEvent = DvmOpReturnRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/opReturn/NIP90OpReturnResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/opReturn/DvmOpReturnResponseEvent.kt similarity index 82% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/opReturn/NIP90OpReturnResponseEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/opReturn/DvmOpReturnResponseEvent.kt index 9e9b8e986c..32007e843e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/opReturn/NIP90OpReturnResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/opReturn/DvmOpReturnResponseEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90OpReturnResponseEvent( +class DvmOpReturnResponseEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -43,9 +43,15 @@ class NIP90OpReturnResponseEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90OpReturnResponseEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmOpReturnResponseEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmOpReturnResponseEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmOpReturnResponseEvent", "com.vitorpamplona.quartz.nip90Dvms.opReturn.DvmOpReturnResponseEvent"), +) +typealias NIP90OpReturnResponseEvent = DvmOpReturnResponseEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/peopleSearch/NIP90PeopleSearchRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/peopleSearch/DvmPeopleSearchRequestEvent.kt similarity index 87% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/peopleSearch/NIP90PeopleSearchRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/peopleSearch/DvmPeopleSearchRequestEvent.kt index d542a5ccc9..a44279270c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/peopleSearch/NIP90PeopleSearchRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/peopleSearch/DvmPeopleSearchRequestEvent.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.quartz.nip90Dvms.tags.param import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90PeopleSearchRequestEvent( +class DvmPeopleSearchRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -67,7 +67,7 @@ class NIP90PeopleSearchRequestEvent( searchQuery: String, maxResults: Int? = null, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90PeopleSearchRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmPeopleSearchRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { inputText(searchQuery) maxResults?.let { param("max_results", it.toString()) } @@ -75,3 +75,9 @@ class NIP90PeopleSearchRequestEvent( } } } + +@Deprecated( + "Renamed to DvmPeopleSearchRequestEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmPeopleSearchRequestEvent", "com.vitorpamplona.quartz.nip90Dvms.peopleSearch.DvmPeopleSearchRequestEvent"), +) +typealias NIP90PeopleSearchRequestEvent = DvmPeopleSearchRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/peopleSearch/NIP90PeopleSearchResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/peopleSearch/DvmPeopleSearchResponseEvent.kt similarity index 83% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/peopleSearch/NIP90PeopleSearchResponseEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/peopleSearch/DvmPeopleSearchResponseEvent.kt index 6156985d53..05c7c163ac 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/peopleSearch/NIP90PeopleSearchResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/peopleSearch/DvmPeopleSearchResponseEvent.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90PeopleSearchResponseEvent( +class DvmPeopleSearchResponseEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -61,7 +61,7 @@ class NIP90PeopleSearchResponseEvent( } } } catch (e: Throwable) { - Log.w("NIP90PeopleSearchResponseEvent") { "Error parsing the JSON ${e.message}" } + Log.w("DvmPeopleSearchResponseEvent") { "Error parsing the JSON ${e.message}" } } return people ?: listOf() @@ -72,9 +72,15 @@ class NIP90PeopleSearchResponseEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90PeopleSearchResponseEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmPeopleSearchResponseEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmPeopleSearchResponseEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmPeopleSearchResponseEvent", "com.vitorpamplona.quartz.nip90Dvms.peopleSearch.DvmPeopleSearchResponseEvent"), +) +typealias NIP90PeopleSearchResponseEvent = DvmPeopleSearchResponseEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/status/NIP90StatusEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/status/DvmStatusEvent.kt similarity index 85% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/status/NIP90StatusEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/status/DvmStatusEvent.kt index c01eb4cc38..e14e41468c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/status/NIP90StatusEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/status/DvmStatusEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90StatusEvent( +class DvmStatusEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -45,9 +45,15 @@ class NIP90StatusEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90StatusEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmStatusEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmStatusEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmStatusEvent", "com.vitorpamplona.quartz.nip90Dvms.status.DvmStatusEvent"), +) +typealias NIP90StatusEvent = DvmStatusEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/status/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/status/TagArrayBuilderExt.kt index ff12777c74..bc919bff57 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/status/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/status/TagArrayBuilderExt.kt @@ -24,14 +24,14 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip90Dvms.status.tags.AmountTag import com.vitorpamplona.quartz.nip90Dvms.status.tags.StatusTag -fun TagArrayBuilder<NIP90StatusEvent>.status( +fun TagArrayBuilder<DvmStatusEvent>.status( code: String, description: String, ) = addUnique(StatusTag.assemble(code, description)) -fun TagArrayBuilder<NIP90StatusEvent>.amount(amount: Long) = addUnique(AmountTag.assemble(amount)) +fun TagArrayBuilder<DvmStatusEvent>.amount(amount: Long) = addUnique(AmountTag.assemble(amount)) -fun TagArrayBuilder<NIP90StatusEvent>.amount( +fun TagArrayBuilder<DvmStatusEvent>.amount( amount: Long, lnInvoice: String, ) = addUnique(AmountTag.assemble(amount, lnInvoice)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/summarization/NIP90SummarizationRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/summarization/DvmSummarizationRequestEvent.kt similarity index 85% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/summarization/NIP90SummarizationRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/summarization/DvmSummarizationRequestEvent.kt index 3e7cf4a0db..aaf23220b0 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/summarization/NIP90SummarizationRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/summarization/DvmSummarizationRequestEvent.kt @@ -35,7 +35,7 @@ import com.vitorpamplona.quartz.nip90Dvms.tags.param import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90SummarizationRequestEvent( +class DvmSummarizationRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -57,7 +57,7 @@ class NIP90SummarizationRequestEvent( length: String? = null, outputMimeType: String? = null, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90SummarizationRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmSummarizationRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { eventIds.forEach { inputEvent(it) } length?.let { param("length", it) } @@ -66,3 +66,9 @@ class NIP90SummarizationRequestEvent( } } } + +@Deprecated( + "Renamed to DvmSummarizationRequestEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmSummarizationRequestEvent", "com.vitorpamplona.quartz.nip90Dvms.summarization.DvmSummarizationRequestEvent"), +) +typealias NIP90SummarizationRequestEvent = DvmSummarizationRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/summarization/NIP90SummarizationResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/summarization/DvmSummarizationResponseEvent.kt similarity index 80% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/summarization/NIP90SummarizationResponseEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/summarization/DvmSummarizationResponseEvent.kt index d4b97ca533..9b04b3ffa0 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/summarization/NIP90SummarizationResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/summarization/DvmSummarizationResponseEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90SummarizationResponseEvent( +class DvmSummarizationResponseEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -41,9 +41,15 @@ class NIP90SummarizationResponseEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90SummarizationResponseEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmSummarizationResponseEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmSummarizationResponseEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmSummarizationResponseEvent", "com.vitorpamplona.quartz.nip90Dvms.summarization.DvmSummarizationResponseEvent"), +) +typealias NIP90SummarizationResponseEvent = DvmSummarizationResponseEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textExtraction/NIP90TextExtractionRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textExtraction/DvmTextExtractionRequestEvent.kt similarity index 87% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textExtraction/NIP90TextExtractionRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textExtraction/DvmTextExtractionRequestEvent.kt index c61c29b542..8ea2ad8cca 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textExtraction/NIP90TextExtractionRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textExtraction/DvmTextExtractionRequestEvent.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.quartz.nip90Dvms.tags.param import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90TextExtractionRequestEvent( +class DvmTextExtractionRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -64,7 +64,7 @@ class NIP90TextExtractionRequestEvent( rangeEnd: String? = null, alignment: String? = null, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90TextExtractionRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmTextExtractionRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { inputUrl(inputUrl) outputMimeType?.let { output(it) } @@ -76,3 +76,9 @@ class NIP90TextExtractionRequestEvent( } } } + +@Deprecated( + "Renamed to DvmTextExtractionRequestEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmTextExtractionRequestEvent", "com.vitorpamplona.quartz.nip90Dvms.textExtraction.DvmTextExtractionRequestEvent"), +) +typealias NIP90TextExtractionRequestEvent = DvmTextExtractionRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textExtraction/NIP90TextExtractionResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textExtraction/DvmTextExtractionResponseEvent.kt similarity index 80% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textExtraction/NIP90TextExtractionResponseEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textExtraction/DvmTextExtractionResponseEvent.kt index ae808faa45..814f311d3e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textExtraction/NIP90TextExtractionResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textExtraction/DvmTextExtractionResponseEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90TextExtractionResponseEvent( +class DvmTextExtractionResponseEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -41,9 +41,15 @@ class NIP90TextExtractionResponseEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90TextExtractionResponseEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmTextExtractionResponseEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmTextExtractionResponseEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmTextExtractionResponseEvent", "com.vitorpamplona.quartz.nip90Dvms.textExtraction.DvmTextExtractionResponseEvent"), +) +typealias NIP90TextExtractionResponseEvent = DvmTextExtractionResponseEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textGeneration/NIP90TextGenerationRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textGeneration/DvmTextGenerationRequestEvent.kt similarity index 90% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textGeneration/NIP90TextGenerationRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textGeneration/DvmTextGenerationRequestEvent.kt index 6554dd73ab..2e0f57fdde 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textGeneration/NIP90TextGenerationRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textGeneration/DvmTextGenerationRequestEvent.kt @@ -37,7 +37,7 @@ import com.vitorpamplona.quartz.nip90Dvms.tags.param import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90TextGenerationRequestEvent( +class DvmTextGenerationRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -88,7 +88,7 @@ class NIP90TextGenerationRequestEvent( frequencyPenalty: Double? = null, outputMimeType: String? = null, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90TextGenerationRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmTextGenerationRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { inputPrompt(prompt) model?.let { param("model", it) } @@ -102,3 +102,9 @@ class NIP90TextGenerationRequestEvent( } } } + +@Deprecated( + "Renamed to DvmTextGenerationRequestEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmTextGenerationRequestEvent", "com.vitorpamplona.quartz.nip90Dvms.textGeneration.DvmTextGenerationRequestEvent"), +) +typealias NIP90TextGenerationRequestEvent = DvmTextGenerationRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textGeneration/NIP90TextGenerationResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textGeneration/DvmTextGenerationResponseEvent.kt similarity index 80% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textGeneration/NIP90TextGenerationResponseEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textGeneration/DvmTextGenerationResponseEvent.kt index ded0df9d9b..2619816759 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textGeneration/NIP90TextGenerationResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textGeneration/DvmTextGenerationResponseEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90TextGenerationResponseEvent( +class DvmTextGenerationResponseEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -41,9 +41,15 @@ class NIP90TextGenerationResponseEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90TextGenerationResponseEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmTextGenerationResponseEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmTextGenerationResponseEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmTextGenerationResponseEvent", "com.vitorpamplona.quartz.nip90Dvms.textGeneration.DvmTextGenerationResponseEvent"), +) +typealias NIP90TextGenerationResponseEvent = DvmTextGenerationResponseEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textToSpeech/NIP90TextToSpeechRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textToSpeech/DvmTextToSpeechRequestEvent.kt similarity index 87% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textToSpeech/NIP90TextToSpeechRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textToSpeech/DvmTextToSpeechRequestEvent.kt index 251fe7b510..41427dbb9e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textToSpeech/NIP90TextToSpeechRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textToSpeech/DvmTextToSpeechRequestEvent.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.quartz.nip90Dvms.tags.param import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90TextToSpeechRequestEvent( +class DvmTextToSpeechRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -67,7 +67,7 @@ class NIP90TextToSpeechRequestEvent( text: String, language: String? = null, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90TextToSpeechRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmTextToSpeechRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { inputText(text) language?.let { param("language", it) } @@ -75,3 +75,9 @@ class NIP90TextToSpeechRequestEvent( } } } + +@Deprecated( + "Renamed to DvmTextToSpeechRequestEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmTextToSpeechRequestEvent", "com.vitorpamplona.quartz.nip90Dvms.textToSpeech.DvmTextToSpeechRequestEvent"), +) +typealias NIP90TextToSpeechRequestEvent = DvmTextToSpeechRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textToSpeech/NIP90TextToSpeechResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textToSpeech/DvmTextToSpeechResponseEvent.kt similarity index 81% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textToSpeech/NIP90TextToSpeechResponseEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textToSpeech/DvmTextToSpeechResponseEvent.kt index 01cbf1e0ca..2a76d0f54a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textToSpeech/NIP90TextToSpeechResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/textToSpeech/DvmTextToSpeechResponseEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90TextToSpeechResponseEvent( +class DvmTextToSpeechResponseEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -41,9 +41,15 @@ class NIP90TextToSpeechResponseEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90TextToSpeechResponseEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmTextToSpeechResponseEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmTextToSpeechResponseEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmTextToSpeechResponseEvent", "com.vitorpamplona.quartz.nip90Dvms.textToSpeech.DvmTextToSpeechResponseEvent"), +) +typealias NIP90TextToSpeechResponseEvent = DvmTextToSpeechResponseEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/translation/NIP90TranslationRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/translation/DvmTranslationRequestEvent.kt similarity index 85% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/translation/NIP90TranslationRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/translation/DvmTranslationRequestEvent.kt index aed285ea54..560842a3cd 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/translation/NIP90TranslationRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/translation/DvmTranslationRequestEvent.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.quartz.nip90Dvms.tags.param import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90TranslationRequestEvent( +class DvmTranslationRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -52,7 +52,7 @@ class NIP90TranslationRequestEvent( eventIds: List<HexKey>, language: String, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90TranslationRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmTranslationRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { eventIds.forEach { inputEvent(it) } param("language", language) @@ -60,3 +60,9 @@ class NIP90TranslationRequestEvent( } } } + +@Deprecated( + "Renamed to DvmTranslationRequestEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmTranslationRequestEvent", "com.vitorpamplona.quartz.nip90Dvms.translation.DvmTranslationRequestEvent"), +) +typealias NIP90TranslationRequestEvent = DvmTranslationRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/translation/NIP90TranslationResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/translation/DvmTranslationResponseEvent.kt similarity index 81% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/translation/NIP90TranslationResponseEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/translation/DvmTranslationResponseEvent.kt index b43ef64283..a051d8a242 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/translation/NIP90TranslationResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/translation/DvmTranslationResponseEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90TranslationResponseEvent( +class DvmTranslationResponseEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -41,9 +41,15 @@ class NIP90TranslationResponseEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90TranslationResponseEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmTranslationResponseEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmTranslationResponseEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmTranslationResponseEvent", "com.vitorpamplona.quartz.nip90Dvms.translation.DvmTranslationResponseEvent"), +) +typealias NIP90TranslationResponseEvent = DvmTranslationResponseEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/userDiscoveryRequest/NIP90UserDiscoveryRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/userDiscoveryRequest/DvmUserDiscoveryRequestEvent.kt similarity index 83% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/userDiscoveryRequest/NIP90UserDiscoveryRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/userDiscoveryRequest/DvmUserDiscoveryRequestEvent.kt index be937e06fe..a3482cd7e7 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/userDiscoveryRequest/NIP90UserDiscoveryRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/userDiscoveryRequest/DvmUserDiscoveryRequestEvent.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.nip90Dvms.tags.inputs import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90UserDiscoveryRequestEvent( +class DvmUserDiscoveryRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -50,9 +50,15 @@ class NIP90UserDiscoveryRequestEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90UserDiscoveryRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmUserDiscoveryRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmUserDiscoveryRequestEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmUserDiscoveryRequestEvent", "com.vitorpamplona.quartz.nip90Dvms.userDiscoveryRequest.DvmUserDiscoveryRequestEvent"), +) +typealias NIP90UserDiscoveryRequestEvent = DvmUserDiscoveryRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/userDiscoveryResponse/NIP90UserDiscoveryResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/userDiscoveryResponse/DvmUserDiscoveryResponseEvent.kt similarity index 82% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/userDiscoveryResponse/NIP90UserDiscoveryResponseEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/userDiscoveryResponse/DvmUserDiscoveryResponseEvent.kt index ba220f40bd..28d3d42e05 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/userDiscoveryResponse/NIP90UserDiscoveryResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/userDiscoveryResponse/DvmUserDiscoveryResponseEvent.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90UserDiscoveryResponseEvent( +class DvmUserDiscoveryResponseEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -61,7 +61,7 @@ class NIP90UserDiscoveryResponseEvent( } } } catch (e: Throwable) { - Log.w("NIP90UserDiscoveryResponseEvent") { "Error parsing the JSON ${e.message}" } + Log.w("DvmUserDiscoveryResponseEvent") { "Error parsing the JSON ${e.message}" } } return people ?: listOf() @@ -72,9 +72,15 @@ class NIP90UserDiscoveryResponseEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90UserDiscoveryResponseEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmUserDiscoveryResponseEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmUserDiscoveryResponseEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmUserDiscoveryResponseEvent", "com.vitorpamplona.quartz.nip90Dvms.userDiscoveryResponse.DvmUserDiscoveryResponseEvent"), +) +typealias NIP90UserDiscoveryResponseEvent = DvmUserDiscoveryResponseEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoConversion/NIP90VideoConversionRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoConversion/DvmVideoConversionRequestEvent.kt similarity index 86% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoConversion/NIP90VideoConversionRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoConversion/DvmVideoConversionRequestEvent.kt index ffc3adc0c5..d438273af9 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoConversion/NIP90VideoConversionRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoConversion/DvmVideoConversionRequestEvent.kt @@ -35,7 +35,7 @@ import com.vitorpamplona.quartz.nip90Dvms.tags.param import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90VideoConversionRequestEvent( +class DvmVideoConversionRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -60,7 +60,7 @@ class NIP90VideoConversionRequestEvent( rangeStart: String? = null, rangeEnd: String? = null, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90VideoConversionRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmVideoConversionRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { inputUrl(videoUrl) outputMimeType?.let { output(it) } @@ -71,3 +71,9 @@ class NIP90VideoConversionRequestEvent( } } } + +@Deprecated( + "Renamed to DvmVideoConversionRequestEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmVideoConversionRequestEvent", "com.vitorpamplona.quartz.nip90Dvms.videoConversion.DvmVideoConversionRequestEvent"), +) +typealias NIP90VideoConversionRequestEvent = DvmVideoConversionRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoConversion/NIP90VideoConversionResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoConversion/DvmVideoConversionResponseEvent.kt similarity index 80% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoConversion/NIP90VideoConversionResponseEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoConversion/DvmVideoConversionResponseEvent.kt index ebd7a9031c..923217e476 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoConversion/NIP90VideoConversionResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoConversion/DvmVideoConversionResponseEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90VideoConversionResponseEvent( +class DvmVideoConversionResponseEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -41,9 +41,15 @@ class NIP90VideoConversionResponseEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90VideoConversionResponseEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmVideoConversionResponseEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmVideoConversionResponseEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmVideoConversionResponseEvent", "com.vitorpamplona.quartz.nip90Dvms.videoConversion.DvmVideoConversionResponseEvent"), +) +typealias NIP90VideoConversionResponseEvent = DvmVideoConversionResponseEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoTranslation/NIP90VideoTranslationRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoTranslation/DvmVideoTranslationRequestEvent.kt similarity index 87% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoTranslation/NIP90VideoTranslationRequestEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoTranslation/DvmVideoTranslationRequestEvent.kt index 859be0382c..7df77e7a7f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoTranslation/NIP90VideoTranslationRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoTranslation/DvmVideoTranslationRequestEvent.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.quartz.nip90Dvms.tags.param import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90VideoTranslationRequestEvent( +class DvmVideoTranslationRequestEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -67,7 +67,7 @@ class NIP90VideoTranslationRequestEvent( rangeEnd: String? = null, outputFormat: String? = null, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90VideoTranslationRequestEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmVideoTranslationRequestEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { inputUrl(videoUrl) param("language", language) @@ -80,3 +80,9 @@ class NIP90VideoTranslationRequestEvent( } } } + +@Deprecated( + "Renamed to DvmVideoTranslationRequestEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmVideoTranslationRequestEvent", "com.vitorpamplona.quartz.nip90Dvms.videoTranslation.DvmVideoTranslationRequestEvent"), +) +typealias NIP90VideoTranslationRequestEvent = DvmVideoTranslationRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoTranslation/NIP90VideoTranslationResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoTranslation/DvmVideoTranslationResponseEvent.kt similarity index 80% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoTranslation/NIP90VideoTranslationResponseEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoTranslation/DvmVideoTranslationResponseEvent.kt index cea768fdd5..a1ee14aa54 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoTranslation/NIP90VideoTranslationResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/videoTranslation/DvmVideoTranslationResponseEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class NIP90VideoTranslationResponseEvent( +class DvmVideoTranslationResponseEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -41,9 +41,15 @@ class NIP90VideoTranslationResponseEvent( fun build( createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<NIP90VideoTranslationResponseEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<DvmVideoTranslationResponseEvent>.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { initializer() } } } + +@Deprecated( + "Renamed to DvmVideoTranslationResponseEvent. NIP-90 Data Vending Machine events use the Dvm prefix.", + ReplaceWith("DvmVideoTranslationResponseEvent", "com.vitorpamplona.quartz.nip90Dvms.videoTranslation.DvmVideoTranslationResponseEvent"), +) +typealias NIP90VideoTranslationResponseEvent = DvmVideoTranslationResponseEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/FileHeaderEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/FileMetadataEvent.kt similarity index 93% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/FileHeaderEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/FileMetadataEvent.kt index 6ac3a389cf..d4e9cfd79d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/FileHeaderEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/FileMetadataEvent.kt @@ -46,7 +46,7 @@ import com.vitorpamplona.quartz.nip94FileMetadata.tags.UrlTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class FileHeaderEvent( +class FileMetadataEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -105,7 +105,7 @@ class FileHeaderEvent( url: String, caption: String?, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<FileHeaderEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<FileMetadataEvent>.() -> Unit = {}, ) = eventTemplate(KIND, caption ?: "", createdAt) { url(url) // NIP-94 accessibility description of the file (kept; the deprecated @@ -127,7 +127,7 @@ class FileHeaderEvent( magnetUri: String? = null, torrentInfoHash: String? = null, createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder<FileHeaderEvent>.() -> Unit = {}, + initializer: TagArrayBuilder<FileMetadataEvent>.() -> Unit = {}, ) = eventTemplate(KIND, caption ?: "", createdAt) { url(url) // NIP-94 accessibility description of the file (kept; the deprecated @@ -148,3 +148,9 @@ class FileHeaderEvent( } } } + +@Deprecated( + "Renamed to FileMetadataEvent. NIP-94 names kind 1063 file metadata.", + ReplaceWith("FileMetadataEvent", "com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent"), +) +typealias FileHeaderEvent = FileMetadataEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/TagArrayBuilderExt.kt index e000583d6e..d89c862235 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/TagArrayBuilderExt.kt @@ -38,32 +38,32 @@ import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbhashTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash import com.vitorpamplona.quartz.nip94FileMetadata.tags.UrlTag -fun TagArrayBuilder<FileHeaderEvent>.url(url: String) = add(UrlTag.assemble(url)) +fun TagArrayBuilder<FileMetadataEvent>.url(url: String) = add(UrlTag.assemble(url)) -fun TagArrayBuilder<FileHeaderEvent>.mimeType(mimeType: String) = add(MimeTypeTag.assemble(mimeType)) +fun TagArrayBuilder<FileMetadataEvent>.mimeType(mimeType: String) = add(MimeTypeTag.assemble(mimeType)) -fun TagArrayBuilder<FileHeaderEvent>.hash(hash: HexKey) = add(HashSha256Tag.assemble(hash)) +fun TagArrayBuilder<FileMetadataEvent>.hash(hash: HexKey) = add(HashSha256Tag.assemble(hash)) -fun TagArrayBuilder<FileHeaderEvent>.fileSize(size: Int) = add(SizeTag.assemble(size)) +fun TagArrayBuilder<FileMetadataEvent>.fileSize(size: Int) = add(SizeTag.assemble(size)) -fun TagArrayBuilder<FileHeaderEvent>.dimension(dim: DimensionTag) = add(DimensionTag.assemble(dim)) +fun TagArrayBuilder<FileMetadataEvent>.dimension(dim: DimensionTag) = add(DimensionTag.assemble(dim)) -fun TagArrayBuilder<FileHeaderEvent>.blurhash(blurhash: String) = add(BlurhashTag.assemble(blurhash)) +fun TagArrayBuilder<FileMetadataEvent>.blurhash(blurhash: String) = add(BlurhashTag.assemble(blurhash)) -fun TagArrayBuilder<FileHeaderEvent>.thumbhash(thumbhash: String) = add(ThumbhashTag.assemble(thumbhash)) +fun TagArrayBuilder<FileMetadataEvent>.thumbhash(thumbhash: String) = add(ThumbhashTag.assemble(thumbhash)) -fun TagArrayBuilder<FileHeaderEvent>.originalHash(hash: HexKey) = add(OriginalHashTag.assemble(hash)) +fun TagArrayBuilder<FileMetadataEvent>.originalHash(hash: HexKey) = add(OriginalHashTag.assemble(hash)) -fun TagArrayBuilder<FileHeaderEvent>.torrentInfohash(hash: String) = add(TorrentInfoHash.assemble(hash)) +fun TagArrayBuilder<FileMetadataEvent>.torrentInfohash(hash: String) = add(TorrentInfoHash.assemble(hash)) -fun TagArrayBuilder<FileHeaderEvent>.magnet(magnetUri: String) = add(MagnetTag.assemble(magnetUri)) +fun TagArrayBuilder<FileMetadataEvent>.magnet(magnetUri: String) = add(MagnetTag.assemble(magnetUri)) -fun TagArrayBuilder<FileHeaderEvent>.image(imageUrl: HexKey) = add(ImageTag.assemble(imageUrl)) +fun TagArrayBuilder<FileMetadataEvent>.image(imageUrl: HexKey) = add(ImageTag.assemble(imageUrl)) -fun TagArrayBuilder<FileHeaderEvent>.thumb(trumbUrl: HexKey) = add(ThumbTag.assemble(trumbUrl)) +fun TagArrayBuilder<FileMetadataEvent>.thumb(trumbUrl: HexKey) = add(ThumbTag.assemble(trumbUrl)) -fun TagArrayBuilder<FileHeaderEvent>.summary(summary: HexKey) = add(SummaryTag.assemble(summary)) +fun TagArrayBuilder<FileMetadataEvent>.summary(summary: HexKey) = add(SummaryTag.assemble(summary)) -fun TagArrayBuilder<FileHeaderEvent>.fallback(fallbackUrl: HexKey) = add(FallbackTag.assemble(fallbackUrl)) +fun TagArrayBuilder<FileMetadataEvent>.fallback(fallbackUrl: HexKey) = add(FallbackTag.assemble(fallbackUrl)) -fun TagArrayBuilder<FileHeaderEvent>.service(service: HexKey) = add(ServiceTag.assemble(service)) +fun TagArrayBuilder<FileMetadataEvent>.service(service: HexKey) = add(ServiceTag.assemble(service)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt index 15d8d83388..d852130e87 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt @@ -181,8 +181,8 @@ import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip15Marketplace.auction.AuctionEvent import com.vitorpamplona.quartz.nip15Marketplace.bid.BidEvent @@ -192,38 +192,38 @@ import com.vitorpamplona.quartz.nip15Marketplace.product.ProductEvent import com.vitorpamplona.quartz.nip15Marketplace.stall.StallEvent import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip25Reactions.ExternalReactionEvent import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelHideMessageEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMuteUserEvent -import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent +import com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupAdminsEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMembersEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupParticipantsEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupPinnedEvent -import com.vitorpamplona.quartz.nip29RelayGroups.metadata.SupportedRolesEvent +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupRolesEvent import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateGroupEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateInviteEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.DeleteEventEvent import com.vitorpamplona.quartz.nip29RelayGroups.moderation.DeleteGroupEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.EditMetadataEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.PutUserEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.RemoveUserEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.UpdatePinListEvent -import com.vitorpamplona.quartz.nip29RelayGroups.request.JoinRequestEvent -import com.vitorpamplona.quartz.nip29RelayGroups.request.LeaveRequestEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupCreateInviteEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupDeleteEventEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupEditMetadataEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupPutUserEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupRemoveUserEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupUpdatePinListEvent +import com.vitorpamplona.quartz.nip29RelayGroups.request.GroupJoinRequestEvent +import com.vitorpamplona.quartz.nip29RelayGroups.request.GroupLeaveRequestEvent import com.vitorpamplona.quartz.nip29RelayGroups.tags.GroupIdTag import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiListEvent import com.vitorpamplona.quartz.nip32Labeling.LabelEvent import com.vitorpamplona.quartz.nip34Git.grasp.UserGraspListEvent import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent @@ -241,7 +241,7 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent -import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent +import com.vitorpamplona.quartz.nip38UserStatus.UserStatusEvent import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip43RelayMembers.addMember.RelayAddMemberEvent @@ -251,10 +251,10 @@ import com.vitorpamplona.quartz.nip43RelayMembers.leaveRequest.RelayLeaveRequest import com.vitorpamplona.quartz.nip43RelayMembers.list.RelayMembershipListEvent import com.vitorpamplona.quartz.nip43RelayMembers.removeMember.RelayRemoveMemberEvent import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcInfoEvent import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcNotificationEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.appCurationSet.AppCurationSetEvent @@ -262,26 +262,26 @@ import com.vitorpamplona.quartz.nip51Lists.articleCurationSet.ArticleCurationSet import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent import com.vitorpamplona.quartz.nip51Lists.gitAuthorList.GitAuthorListEvent import com.vitorpamplona.quartz.nip51Lists.gitRepositoryList.GitRepositoryListEvent import com.vitorpamplona.quartz.nip51Lists.goodWikiAuthorList.GoodWikiAuthorListEvent import com.vitorpamplona.quartz.nip51Lists.goodWikiRelayList.GoodWikiRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent import com.vitorpamplona.quartz.nip51Lists.kindMuteSet.KindMuteSetEvent -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent import com.vitorpamplona.quartz.nip51Lists.mediaFollowList.MediaFollowListEvent import com.vitorpamplona.quartz.nip51Lists.mediaStarterPack.MediaStarterPackEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.pictureCurationSet.PictureCurationSetEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent @@ -289,7 +289,7 @@ import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent import com.vitorpamplona.quartz.nip51Lists.videoCurationSet.VideoCurationSetEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import com.vitorpamplona.quartz.nip52Calendar.rsvp.CalendarRSVPEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent @@ -299,19 +299,19 @@ import com.vitorpamplona.quartz.nip53LiveActivities.nestsServers.NestsServersEve import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent import com.vitorpamplona.quartz.nip53LiveActivities.raid.LiveActivitiesRaidEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip54Wiki.WikiMergeAcceptanceEvent import com.vitorpamplona.quartz.nip54Wiki.WikiMergeRequestEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip54Wiki.WikiRedirectEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.EphemeralGiftWrapEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent @@ -325,8 +325,6 @@ import com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenEvent import com.vitorpamplona.quartz.nip60Cashu.wallet.CashuWalletEvent import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent -import com.vitorpamplona.quartz.nip61Nutzaps.redemption.NutzapRedemptionEvent -import com.vitorpamplona.quartz.nip61Nutzaps.token.TokenEvent import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent import com.vitorpamplona.quartz.nip64Chess.challenge.accept.LiveChessGameAcceptEvent import com.vitorpamplona.quartz.nip64Chess.challenge.offer.LiveChessGameChallengeEvent @@ -340,16 +338,16 @@ import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.RelayDiscoveryEvent import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.RelayMonitorEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip69P2pOrderEvents.P2POrderEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent +import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent +import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoShortEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip71Video.textTrack.TextTrackEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent import com.vitorpamplona.quartz.nip72ModCommunities.rules.CommunityRulesEvent -import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent +import com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent import com.vitorpamplona.quartz.nip78AppData.AppDataEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent @@ -358,7 +356,7 @@ import com.vitorpamplona.quartz.nip85TrustedAssertions.addressables.AddressableA import com.vitorpamplona.quartz.nip85TrustedAssertions.events.EventAssertionEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.externalIds.ExternalIdAssertionEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent -import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent import com.vitorpamplona.quartz.nip87Ecash.fedimint.FedimintEvent import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent @@ -366,47 +364,47 @@ import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.NIP90ContentDiscoveryRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.NIP90ContentDiscoveryResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.contentSearch.NIP90ContentSearchRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.contentSearch.NIP90ContentSearchResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryRequest.DvmContentDiscoveryRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.contentSearch.DvmContentSearchRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.contentSearch.DvmContentSearchResponseEvent import com.vitorpamplona.quartz.nip90Dvms.dvmHeartbeat.DvmHeartbeatEvent -import com.vitorpamplona.quartz.nip90Dvms.eventCount.NIP90EventCountRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.eventCount.NIP90EventCountResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.eventPowDelegation.NIP90EventPowDelegationRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.eventPowDelegation.NIP90EventPowDelegationResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.eventPublishSchedule.NIP90EventPublishScheduleRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.eventPublishSchedule.NIP90EventPublishScheduleResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.eventTimestamping.NIP90EventTimestampingRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.eventTimestamping.NIP90EventTimestampingResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.imageGeneration.NIP90ImageGenerationRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.imageGeneration.NIP90ImageGenerationResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.imageToVideo.NIP90ImageToVideoRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.imageToVideo.NIP90ImageToVideoResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.malwareScanning.NIP90MalwareScanRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.malwareScanning.NIP90MalwareScanResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.opReturn.NIP90OpReturnRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.opReturn.NIP90OpReturnResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.peopleSearch.NIP90PeopleSearchRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.peopleSearch.NIP90PeopleSearchResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.status.NIP90StatusEvent -import com.vitorpamplona.quartz.nip90Dvms.summarization.NIP90SummarizationRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.summarization.NIP90SummarizationResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.textExtraction.NIP90TextExtractionRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.textExtraction.NIP90TextExtractionResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.textGeneration.NIP90TextGenerationRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.textGeneration.NIP90TextGenerationResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.textToSpeech.NIP90TextToSpeechRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.textToSpeech.NIP90TextToSpeechResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.translation.NIP90TranslationRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.translation.NIP90TranslationResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryRequest.NIP90UserDiscoveryRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryResponse.NIP90UserDiscoveryResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.videoConversion.NIP90VideoConversionRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.videoConversion.NIP90VideoConversionResponseEvent -import com.vitorpamplona.quartz.nip90Dvms.videoTranslation.NIP90VideoTranslationRequestEvent -import com.vitorpamplona.quartz.nip90Dvms.videoTranslation.NIP90VideoTranslationResponseEvent -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import com.vitorpamplona.quartz.nip90Dvms.eventCount.DvmEventCountRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.eventCount.DvmEventCountResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.eventPowDelegation.DvmEventPowDelegationRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.eventPowDelegation.DvmEventPowDelegationResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.eventPublishSchedule.DvmEventPublishScheduleRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.eventPublishSchedule.DvmEventPublishScheduleResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.eventTimestamping.DvmEventTimestampingRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.eventTimestamping.DvmEventTimestampingResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.imageGeneration.DvmImageGenerationRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.imageGeneration.DvmImageGenerationResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.imageToVideo.DvmImageToVideoRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.imageToVideo.DvmImageToVideoResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.malwareScanning.DvmMalwareScanRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.malwareScanning.DvmMalwareScanResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.opReturn.DvmOpReturnRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.opReturn.DvmOpReturnResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.peopleSearch.DvmPeopleSearchRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.peopleSearch.DvmPeopleSearchResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.status.DvmStatusEvent +import com.vitorpamplona.quartz.nip90Dvms.summarization.DvmSummarizationRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.summarization.DvmSummarizationResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.textExtraction.DvmTextExtractionRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.textExtraction.DvmTextExtractionResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.textGeneration.DvmTextGenerationRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.textGeneration.DvmTextGenerationResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.textToSpeech.DvmTextToSpeechRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.textToSpeech.DvmTextToSpeechResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.translation.DvmTranslationRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.translation.DvmTranslationResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryRequest.DvmUserDiscoveryRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.userDiscoveryResponse.DvmUserDiscoveryResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.videoConversion.DvmVideoConversionRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.videoConversion.DvmVideoConversionResponseEvent +import com.vitorpamplona.quartz.nip90Dvms.videoTranslation.DvmVideoTranslationRequestEvent +import com.vitorpamplona.quartz.nip90Dvms.videoTranslation.DvmVideoTranslationResponseEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent @@ -570,7 +568,7 @@ class EventFactory { BookmarkListEvent.KIND -> BookmarkListEvent(id, pubKey, createdAt, tags, content, sig) OldBookmarkListEvent.KIND -> OldBookmarkListEvent(id, pubKey, createdAt, tags, content, sig) CalendarDateSlotEvent.KIND -> CalendarDateSlotEvent(id, pubKey, createdAt, tags, content, sig) - CalendarEvent.KIND -> CalendarEvent(id, pubKey, createdAt, tags, content, sig) + CalendarCollectionEvent.KIND -> CalendarCollectionEvent(id, pubKey, createdAt, tags, content, sig) CalendarTimeSlotEvent.KIND -> CalendarTimeSlotEvent(id, pubKey, createdAt, tags, content, sig) CalendarRSVPEvent.KIND -> CalendarRSVPEvent(id, pubKey, createdAt, tags, content, sig) CallAnswerEvent.KIND -> CallAnswerEvent(id, pubKey, createdAt, tags, content, sig) @@ -585,19 +583,19 @@ class EventFactory { CashuSpendingHistoryEvent.KIND -> CashuSpendingHistoryEvent(id, pubKey, createdAt, tags, content, sig) CashuWalletEvent.KIND -> CashuWalletEvent(id, pubKey, createdAt, tags, content, sig) ChatEvent.KIND -> ChatEvent(id, pubKey, createdAt, tags, content, sig) - PutUserEvent.KIND -> PutUserEvent(id, pubKey, createdAt, tags, content, sig) - RemoveUserEvent.KIND -> RemoveUserEvent(id, pubKey, createdAt, tags, content, sig) - EditMetadataEvent.KIND -> EditMetadataEvent(id, pubKey, createdAt, tags, content, sig) - DeleteEventEvent.KIND -> DeleteEventEvent(id, pubKey, createdAt, tags, content, sig) + GroupPutUserEvent.KIND -> GroupPutUserEvent(id, pubKey, createdAt, tags, content, sig) + GroupRemoveUserEvent.KIND -> GroupRemoveUserEvent(id, pubKey, createdAt, tags, content, sig) + GroupEditMetadataEvent.KIND -> GroupEditMetadataEvent(id, pubKey, createdAt, tags, content, sig) + GroupDeleteEventEvent.KIND -> GroupDeleteEventEvent(id, pubKey, createdAt, tags, content, sig) CreateGroupEvent.KIND -> CreateGroupEvent(id, pubKey, createdAt, tags, content, sig) DeleteGroupEvent.KIND -> DeleteGroupEvent(id, pubKey, createdAt, tags, content, sig) - CreateInviteEvent.KIND -> CreateInviteEvent(id, pubKey, createdAt, tags, content, sig) - JoinRequestEvent.KIND -> JoinRequestEvent(id, pubKey, createdAt, tags, content, sig) - LeaveRequestEvent.KIND -> LeaveRequestEvent(id, pubKey, createdAt, tags, content, sig) + GroupCreateInviteEvent.KIND -> GroupCreateInviteEvent(id, pubKey, createdAt, tags, content, sig) + GroupJoinRequestEvent.KIND -> GroupJoinRequestEvent(id, pubKey, createdAt, tags, content, sig) + GroupLeaveRequestEvent.KIND -> GroupLeaveRequestEvent(id, pubKey, createdAt, tags, content, sig) GroupMetadataEvent.KIND -> GroupMetadataEvent(id, pubKey, createdAt, tags, content, sig) GroupAdminsEvent.KIND -> GroupAdminsEvent(id, pubKey, createdAt, tags, content, sig) GroupMembersEvent.KIND -> GroupMembersEvent(id, pubKey, createdAt, tags, content, sig) - SupportedRolesEvent.KIND -> SupportedRolesEvent(id, pubKey, createdAt, tags, content, sig) + GroupRolesEvent.KIND -> GroupRolesEvent(id, pubKey, createdAt, tags, content, sig) GroupParticipantsEvent.KIND -> GroupParticipantsEvent(id, pubKey, createdAt, tags, content, sig) // kind:39005 is shared by two relay-signed, addressable events that never coexist on // one relay: NIP-29's group pin list (relay29 family) and Buzz's NIP-CW thread @@ -618,10 +616,10 @@ class EventFactory { } else { GroupPinnedEvent(id, pubKey, createdAt, tags, content, sig) } - UpdatePinListEvent.KIND -> UpdatePinListEvent(id, pubKey, createdAt, tags, content, sig) + GroupUpdatePinListEvent.KIND -> GroupUpdatePinListEvent(id, pubKey, createdAt, tags, content, sig) ChessGameEvent.KIND -> ChessGameEvent(id, pubKey, createdAt, tags, content, sig) CodeSnippetEvent.KIND -> CodeSnippetEvent(id, pubKey, createdAt, tags, content, sig) - RelayFeedsListEvent.KIND -> RelayFeedsListEvent(id, pubKey, createdAt, tags, content, sig) + FavoriteRelayListEvent.KIND -> FavoriteRelayListEvent(id, pubKey, createdAt, tags, content, sig) JesterEvent.KIND -> JesterEvent(id, pubKey, createdAt, tags, content, sig) LiveChessGameChallengeEvent.KIND -> LiveChessGameChallengeEvent(id, pubKey, createdAt, tags, content, sig) LiveChessGameAcceptEvent.KIND -> LiveChessGameAcceptEvent(id, pubKey, createdAt, tags, content, sig) @@ -630,13 +628,13 @@ class EventFactory { LiveChessDrawOfferEvent.KIND -> LiveChessDrawOfferEvent(id, pubKey, createdAt, tags, content, sig) ChannelCreateEvent.KIND -> ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig) ChannelHideMessageEvent.KIND -> ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig) - ChannelListEvent.KIND -> ChannelListEvent(id, pubKey, createdAt, tags, content, sig) + PublicChatListEvent.KIND -> PublicChatListEvent(id, pubKey, createdAt, tags, content, sig) ChannelMessageEvent.KIND -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig) ChannelMetadataEvent.KIND -> ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig) ChannelMuteUserEvent.KIND -> ChannelMuteUserEvent(id, pubKey, createdAt, tags, content, sig) ChatMessageEncryptedFileHeaderEvent.KIND -> ChatMessageEncryptedFileHeaderEvent(id.ifBlank { EventHasher.hashId(pubKey, createdAt, kind, tags, content) }, pubKey, createdAt, tags, content, sig) ChatMessageEvent.KIND -> ChatMessageEvent(id.ifBlank { EventHasher.hashId(pubKey, createdAt, kind, tags, content) }, pubKey, createdAt, tags, content, sig) - ChatMessageRelayListEvent.KIND -> ChatMessageRelayListEvent(id, pubKey, createdAt, tags, content, sig) + DmRelayListEvent.KIND -> DmRelayListEvent(id, pubKey, createdAt, tags, content, sig) ClassifiedsEvent.KIND -> ClassifiedsEvent(id, pubKey, createdAt, tags, content, sig) CommentEvent.KIND -> CommentEvent(id, pubKey, createdAt, tags, content, sig) CommunityDefinitionEvent.KIND -> CommunityDefinitionEvent(id, pubKey, createdAt, tags, content, sig) @@ -644,21 +642,21 @@ class EventFactory { CommunityPostApprovalEvent.KIND -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig) CommunityRulesEvent.KIND -> CommunityRulesEvent(id, pubKey, createdAt, tags, content, sig) ContactListEvent.KIND -> ContactListEvent(id, pubKey, createdAt, tags, content, sig) - DeletionEvent.KIND -> DeletionEvent(id, pubKey, createdAt, tags, content, sig) + DeletionRequestEvent.KIND -> DeletionRequestEvent(id, pubKey, createdAt, tags, content, sig) DraftWrapEvent.KIND -> DraftWrapEvent(id, pubKey, createdAt, tags, content, sig) EmojiPackEvent.KIND -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig) - EmojiPackSelectionEvent.KIND -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig) + EmojiListEvent.KIND -> EmojiListEvent(id, pubKey, createdAt, tags, content, sig) EphemeralChatEvent.KIND -> EphemeralChatEvent(id, pubKey, createdAt, tags, content, sig) EphemeralChatListEvent.KIND -> EphemeralChatListEvent(id, pubKey, createdAt, tags, content, sig) ExternalIdentitiesEvent.KIND -> ExternalIdentitiesEvent(id, pubKey, createdAt, tags, content, sig) FedimintEvent.KIND -> FedimintEvent(id, pubKey, createdAt, tags, content, sig) - FileHeaderEvent.KIND -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig) + FileMetadataEvent.KIND -> FileMetadataEvent(id, pubKey, createdAt, tags, content, sig) ProfileGalleryEntryEvent.KIND -> ProfileGalleryEntryEvent(id, pubKey, createdAt, tags, content, sig) FileServersEvent.KIND -> FileServersEvent(id, pubKey, createdAt, tags, content, sig) FileStorageEvent.KIND -> FileStorageEvent(id, pubKey, createdAt, tags, content, sig) FileStorageHeaderEvent.KIND -> FileStorageHeaderEvent(id, pubKey, createdAt, tags, content, sig) FhirResourceEvent.KIND -> FhirResourceEvent(id, pubKey, createdAt, tags, content, sig) - FollowListEvent.KIND -> FollowListEvent(id, pubKey, createdAt, tags, content, sig) + StarterPackEvent.KIND -> StarterPackEvent(id, pubKey, createdAt, tags, content, sig) FundraiserEvent.KIND -> FundraiserEvent(id, pubKey, createdAt, tags, content, sig) GenericRepostEvent.KIND -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig) CyberspaceBagEvent.KIND -> CyberspaceBagEvent(id, pubKey, createdAt, tags, content, sig) @@ -701,9 +699,9 @@ class EventFactory { UserGraspListEvent.KIND -> UserGraspListEvent(id, pubKey, createdAt, tags, content, sig) GoodWikiAuthorListEvent.KIND -> GoodWikiAuthorListEvent(id, pubKey, createdAt, tags, content, sig) GoodWikiRelayListEvent.KIND -> GoodWikiRelayListEvent(id, pubKey, createdAt, tags, content, sig) - GoalEvent.KIND -> GoalEvent(id, pubKey, createdAt, tags, content, sig) + ZapGoalEvent.KIND -> ZapGoalEvent(id, pubKey, createdAt, tags, content, sig) FavoriteAlgoFeedsListEvent.KIND -> FavoriteAlgoFeedsListEvent(id, pubKey, createdAt, tags, content, sig) - HashtagListEvent.KIND -> HashtagListEvent(id, pubKey, createdAt, tags, content, sig) + InterestListEvent.KIND -> InterestListEvent(id, pubKey, createdAt, tags, content, sig) HighlightEvent.KIND -> HighlightEvent(id, pubKey, createdAt, tags, content, sig) HTTPAuthorizationEvent.KIND -> HTTPAuthorizationEvent(id, pubKey, createdAt, tags, content, sig) IndexerRelayListEvent.KIND -> IndexerRelayListEvent(id, pubKey, createdAt, tags, content, sig) @@ -713,14 +711,14 @@ class EventFactory { InteractiveStoryReadingStateEvent.KIND -> InteractiveStoryReadingStateEvent(id, pubKey, createdAt, tags, content, sig) LabelEvent.KIND -> LabelEvent(id, pubKey, createdAt, tags, content, sig) KindMuteSetEvent.KIND -> KindMuteSetEvent(id, pubKey, createdAt, tags, content, sig) - LabeledBookmarkListEvent.KIND -> LabeledBookmarkListEvent(id, pubKey, createdAt, tags, content, sig) + BookmarkSetEvent.KIND -> BookmarkSetEvent(id, pubKey, createdAt, tags, content, sig) LiveActivitiesChatMessageEvent.KIND -> LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig) LiveActivitiesClipEvent.KIND -> LiveActivitiesClipEvent(id, pubKey, createdAt, tags, content, sig) LiveActivitiesEvent.KIND -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig) LiveActivitiesRaidEvent.KIND -> LiveActivitiesRaidEvent(id, pubKey, createdAt, tags, content, sig) - LnZapEvent.KIND -> LnZapEvent(id, pubKey, createdAt, tags, content, sig) - LnZapPaymentRequestEvent.KIND -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig) - LnZapPaymentResponseEvent.KIND -> LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig) + ZapReceiptEvent.KIND -> ZapReceiptEvent(id, pubKey, createdAt, tags, content, sig) + NwcRequestEvent.KIND -> NwcRequestEvent(id, pubKey, createdAt, tags, content, sig) + NwcResponseEvent.KIND -> NwcResponseEvent(id, pubKey, createdAt, tags, content, sig) OfferEvent.KIND -> OfferEvent(id, pubKey, createdAt, tags, content, sig) DebitEvent.KIND -> DebitEvent(id, pubKey, createdAt, tags, content, sig) ManageEvent.KIND -> ManageEvent(id, pubKey, createdAt, tags, content, sig) @@ -728,8 +726,8 @@ class EventFactory { NwcNotificationEvent.KIND -> NwcNotificationEvent(id, pubKey, createdAt, tags, content, sig) NwcNotificationEvent.LEGACY_KIND -> NwcNotificationEvent(id, pubKey, createdAt, tags, content, sig) LnZapPrivateEvent.KIND -> LnZapPrivateEvent(id, pubKey, createdAt, tags, content, sig) - LnZapRequestEvent.KIND -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig) - LongTextNoteEvent.KIND -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig) + ZapRequestEvent.KIND -> ZapRequestEvent(id, pubKey, createdAt, tags, content, sig) + LongFormContentEvent.KIND -> LongFormContentEvent(id, pubKey, createdAt, tags, content, sig) MarketplaceEvent.KIND -> MarketplaceEvent(id, pubKey, createdAt, tags, content, sig) MeetingRoomEvent.KIND -> MeetingRoomEvent(id, pubKey, createdAt, tags, content, sig) MeetingRoomPresenceEvent.KIND -> MeetingRoomPresenceEvent(id, pubKey, createdAt, tags, content, sig) @@ -758,55 +756,54 @@ class EventFactory { NipTextEvent.KIND -> NipTextEvent(id, pubKey, createdAt, tags, content, sig) NutzapEvent.KIND -> NutzapEvent(id, pubKey, createdAt, tags, content, sig) NutzapInfoEvent.KIND -> NutzapInfoEvent(id, pubKey, createdAt, tags, content, sig) - NutzapRedemptionEvent.KIND -> NutzapRedemptionEvent(id, pubKey, createdAt, tags, content, sig) NostrConnectEvent.KIND -> NostrConnectEvent(id, pubKey, createdAt, tags, content, sig) - NIP90StatusEvent.KIND -> NIP90StatusEvent(id, pubKey, createdAt, tags, content, sig) + DvmStatusEvent.KIND -> DvmStatusEvent(id, pubKey, createdAt, tags, content, sig) DvmHeartbeatEvent.KIND -> DvmHeartbeatEvent(id, pubKey, createdAt, tags, content, sig) - NIP90TextExtractionRequestEvent.KIND -> NIP90TextExtractionRequestEvent(id, pubKey, createdAt, tags, content, sig) - NIP90TextExtractionResponseEvent.KIND -> NIP90TextExtractionResponseEvent(id, pubKey, createdAt, tags, content, sig) - NIP90SummarizationRequestEvent.KIND -> NIP90SummarizationRequestEvent(id, pubKey, createdAt, tags, content, sig) - NIP90SummarizationResponseEvent.KIND -> NIP90SummarizationResponseEvent(id, pubKey, createdAt, tags, content, sig) - NIP90TranslationRequestEvent.KIND -> NIP90TranslationRequestEvent(id, pubKey, createdAt, tags, content, sig) - NIP90TranslationResponseEvent.KIND -> NIP90TranslationResponseEvent(id, pubKey, createdAt, tags, content, sig) - NIP90TextGenerationRequestEvent.KIND -> NIP90TextGenerationRequestEvent(id, pubKey, createdAt, tags, content, sig) - NIP90TextGenerationResponseEvent.KIND -> NIP90TextGenerationResponseEvent(id, pubKey, createdAt, tags, content, sig) - NIP90ImageGenerationRequestEvent.KIND -> NIP90ImageGenerationRequestEvent(id, pubKey, createdAt, tags, content, sig) - NIP90ImageGenerationResponseEvent.KIND -> NIP90ImageGenerationResponseEvent(id, pubKey, createdAt, tags, content, sig) - NIP90VideoConversionRequestEvent.KIND -> NIP90VideoConversionRequestEvent(id, pubKey, createdAt, tags, content, sig) - NIP90VideoConversionResponseEvent.KIND -> NIP90VideoConversionResponseEvent(id, pubKey, createdAt, tags, content, sig) - NIP90VideoTranslationRequestEvent.KIND -> NIP90VideoTranslationRequestEvent(id, pubKey, createdAt, tags, content, sig) - NIP90VideoTranslationResponseEvent.KIND -> NIP90VideoTranslationResponseEvent(id, pubKey, createdAt, tags, content, sig) - NIP90ImageToVideoRequestEvent.KIND -> NIP90ImageToVideoRequestEvent(id, pubKey, createdAt, tags, content, sig) - NIP90ImageToVideoResponseEvent.KIND -> NIP90ImageToVideoResponseEvent(id, pubKey, createdAt, tags, content, sig) - NIP90TextToSpeechRequestEvent.KIND -> NIP90TextToSpeechRequestEvent(id, pubKey, createdAt, tags, content, sig) - NIP90TextToSpeechResponseEvent.KIND -> NIP90TextToSpeechResponseEvent(id, pubKey, createdAt, tags, content, sig) - NIP90ContentDiscoveryRequestEvent.KIND -> NIP90ContentDiscoveryRequestEvent(id, pubKey, createdAt, tags, content, sig) - NIP90ContentDiscoveryResponseEvent.KIND -> NIP90ContentDiscoveryResponseEvent(id, pubKey, createdAt, tags, content, sig) - NIP90UserDiscoveryRequestEvent.KIND -> NIP90UserDiscoveryRequestEvent(id, pubKey, createdAt, tags, content, sig) - NIP90UserDiscoveryResponseEvent.KIND -> NIP90UserDiscoveryResponseEvent(id, pubKey, createdAt, tags, content, sig) - NIP90ContentSearchRequestEvent.KIND -> NIP90ContentSearchRequestEvent(id, pubKey, createdAt, tags, content, sig) - NIP90ContentSearchResponseEvent.KIND -> NIP90ContentSearchResponseEvent(id, pubKey, createdAt, tags, content, sig) - NIP90PeopleSearchRequestEvent.KIND -> NIP90PeopleSearchRequestEvent(id, pubKey, createdAt, tags, content, sig) - NIP90PeopleSearchResponseEvent.KIND -> NIP90PeopleSearchResponseEvent(id, pubKey, createdAt, tags, content, sig) - NIP90EventCountRequestEvent.KIND -> NIP90EventCountRequestEvent(id, pubKey, createdAt, tags, content, sig) - NIP90EventCountResponseEvent.KIND -> NIP90EventCountResponseEvent(id, pubKey, createdAt, tags, content, sig) - NIP90MalwareScanRequestEvent.KIND -> NIP90MalwareScanRequestEvent(id, pubKey, createdAt, tags, content, sig) - NIP90MalwareScanResponseEvent.KIND -> NIP90MalwareScanResponseEvent(id, pubKey, createdAt, tags, content, sig) - NIP90EventTimestampingRequestEvent.KIND -> NIP90EventTimestampingRequestEvent(id, pubKey, createdAt, tags, content, sig) - NIP90EventTimestampingResponseEvent.KIND -> NIP90EventTimestampingResponseEvent(id, pubKey, createdAt, tags, content, sig) - NIP90OpReturnRequestEvent.KIND -> NIP90OpReturnRequestEvent(id, pubKey, createdAt, tags, content, sig) - NIP90OpReturnResponseEvent.KIND -> NIP90OpReturnResponseEvent(id, pubKey, createdAt, tags, content, sig) - NIP90EventPublishScheduleRequestEvent.KIND -> NIP90EventPublishScheduleRequestEvent(id, pubKey, createdAt, tags, content, sig) - NIP90EventPublishScheduleResponseEvent.KIND -> NIP90EventPublishScheduleResponseEvent(id, pubKey, createdAt, tags, content, sig) - NIP90EventPowDelegationRequestEvent.KIND -> NIP90EventPowDelegationRequestEvent(id, pubKey, createdAt, tags, content, sig) - NIP90EventPowDelegationResponseEvent.KIND -> NIP90EventPowDelegationResponseEvent(id, pubKey, createdAt, tags, content, sig) + DvmTextExtractionRequestEvent.KIND -> DvmTextExtractionRequestEvent(id, pubKey, createdAt, tags, content, sig) + DvmTextExtractionResponseEvent.KIND -> DvmTextExtractionResponseEvent(id, pubKey, createdAt, tags, content, sig) + DvmSummarizationRequestEvent.KIND -> DvmSummarizationRequestEvent(id, pubKey, createdAt, tags, content, sig) + DvmSummarizationResponseEvent.KIND -> DvmSummarizationResponseEvent(id, pubKey, createdAt, tags, content, sig) + DvmTranslationRequestEvent.KIND -> DvmTranslationRequestEvent(id, pubKey, createdAt, tags, content, sig) + DvmTranslationResponseEvent.KIND -> DvmTranslationResponseEvent(id, pubKey, createdAt, tags, content, sig) + DvmTextGenerationRequestEvent.KIND -> DvmTextGenerationRequestEvent(id, pubKey, createdAt, tags, content, sig) + DvmTextGenerationResponseEvent.KIND -> DvmTextGenerationResponseEvent(id, pubKey, createdAt, tags, content, sig) + DvmImageGenerationRequestEvent.KIND -> DvmImageGenerationRequestEvent(id, pubKey, createdAt, tags, content, sig) + DvmImageGenerationResponseEvent.KIND -> DvmImageGenerationResponseEvent(id, pubKey, createdAt, tags, content, sig) + DvmVideoConversionRequestEvent.KIND -> DvmVideoConversionRequestEvent(id, pubKey, createdAt, tags, content, sig) + DvmVideoConversionResponseEvent.KIND -> DvmVideoConversionResponseEvent(id, pubKey, createdAt, tags, content, sig) + DvmVideoTranslationRequestEvent.KIND -> DvmVideoTranslationRequestEvent(id, pubKey, createdAt, tags, content, sig) + DvmVideoTranslationResponseEvent.KIND -> DvmVideoTranslationResponseEvent(id, pubKey, createdAt, tags, content, sig) + DvmImageToVideoRequestEvent.KIND -> DvmImageToVideoRequestEvent(id, pubKey, createdAt, tags, content, sig) + DvmImageToVideoResponseEvent.KIND -> DvmImageToVideoResponseEvent(id, pubKey, createdAt, tags, content, sig) + DvmTextToSpeechRequestEvent.KIND -> DvmTextToSpeechRequestEvent(id, pubKey, createdAt, tags, content, sig) + DvmTextToSpeechResponseEvent.KIND -> DvmTextToSpeechResponseEvent(id, pubKey, createdAt, tags, content, sig) + DvmContentDiscoveryRequestEvent.KIND -> DvmContentDiscoveryRequestEvent(id, pubKey, createdAt, tags, content, sig) + DvmContentDiscoveryResponseEvent.KIND -> DvmContentDiscoveryResponseEvent(id, pubKey, createdAt, tags, content, sig) + DvmUserDiscoveryRequestEvent.KIND -> DvmUserDiscoveryRequestEvent(id, pubKey, createdAt, tags, content, sig) + DvmUserDiscoveryResponseEvent.KIND -> DvmUserDiscoveryResponseEvent(id, pubKey, createdAt, tags, content, sig) + DvmContentSearchRequestEvent.KIND -> DvmContentSearchRequestEvent(id, pubKey, createdAt, tags, content, sig) + DvmContentSearchResponseEvent.KIND -> DvmContentSearchResponseEvent(id, pubKey, createdAt, tags, content, sig) + DvmPeopleSearchRequestEvent.KIND -> DvmPeopleSearchRequestEvent(id, pubKey, createdAt, tags, content, sig) + DvmPeopleSearchResponseEvent.KIND -> DvmPeopleSearchResponseEvent(id, pubKey, createdAt, tags, content, sig) + DvmEventCountRequestEvent.KIND -> DvmEventCountRequestEvent(id, pubKey, createdAt, tags, content, sig) + DvmEventCountResponseEvent.KIND -> DvmEventCountResponseEvent(id, pubKey, createdAt, tags, content, sig) + DvmMalwareScanRequestEvent.KIND -> DvmMalwareScanRequestEvent(id, pubKey, createdAt, tags, content, sig) + DvmMalwareScanResponseEvent.KIND -> DvmMalwareScanResponseEvent(id, pubKey, createdAt, tags, content, sig) + DvmEventTimestampingRequestEvent.KIND -> DvmEventTimestampingRequestEvent(id, pubKey, createdAt, tags, content, sig) + DvmEventTimestampingResponseEvent.KIND -> DvmEventTimestampingResponseEvent(id, pubKey, createdAt, tags, content, sig) + DvmOpReturnRequestEvent.KIND -> DvmOpReturnRequestEvent(id, pubKey, createdAt, tags, content, sig) + DvmOpReturnResponseEvent.KIND -> DvmOpReturnResponseEvent(id, pubKey, createdAt, tags, content, sig) + DvmEventPublishScheduleRequestEvent.KIND -> DvmEventPublishScheduleRequestEvent(id, pubKey, createdAt, tags, content, sig) + DvmEventPublishScheduleResponseEvent.KIND -> DvmEventPublishScheduleResponseEvent(id, pubKey, createdAt, tags, content, sig) + DvmEventPowDelegationRequestEvent.KIND -> DvmEventPowDelegationRequestEvent(id, pubKey, createdAt, tags, content, sig) + DvmEventPowDelegationResponseEvent.KIND -> DvmEventPowDelegationResponseEvent(id, pubKey, createdAt, tags, content, sig) OnchainZapEvent.KIND -> OnchainZapEvent(id, pubKey, createdAt, tags, content, sig) Bolt12ZapEvent.KIND -> Bolt12ZapEvent(id, pubKey, createdAt, tags, content, sig) Bolt12ZapIntentEvent.KIND -> Bolt12ZapIntentEvent(id, pubKey, createdAt, tags, content, sig) Bolt12OfferListEvent.KIND -> Bolt12OfferListEvent(id, pubKey, createdAt, tags, content, sig) OtsEvent.KIND -> OtsEvent(id, pubKey, createdAt, tags, content, sig) PaymentTargetsEvent.KIND -> PaymentTargetsEvent(id, pubKey, createdAt, tags, content, sig) - PeopleListEvent.KIND -> PeopleListEvent(id, pubKey, createdAt, tags, content, sig) + FollowSetEvent.KIND -> FollowSetEvent(id, pubKey, createdAt, tags, content, sig) PictureCurationSetEvent.KIND -> PictureCurationSetEvent(id, pubKey, createdAt, tags, content, sig) P2POrderEvent.KIND -> P2POrderEvent(id, pubKey, createdAt, tags, content, sig) PictureEvent.KIND -> PictureEvent(id, pubKey, createdAt, tags, content, sig) @@ -823,7 +820,7 @@ class EventFactory { Podcasting20EpisodeEvent.KIND -> Podcasting20EpisodeEvent(id, pubKey, createdAt, tags, content, sig) Podcasting20TrailerEvent.KIND -> Podcasting20TrailerEvent(id, pubKey, createdAt, tags, content, sig) ProductEvent.KIND -> ProductEvent(id, pubKey, createdAt, tags, content, sig) - PrivateDmEvent.KIND -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig) + EncryptedDmEvent.KIND -> EncryptedDmEvent(id, pubKey, createdAt, tags, content, sig) PrivateOutboxRelayListEvent.KIND -> PrivateOutboxRelayListEvent(id, pubKey, createdAt, tags, content, sig) ProxyRelayListEvent.KIND -> ProxyRelayListEvent(id, pubKey, createdAt, tags, content, sig) PublicMessageEvent.KIND -> PublicMessageEvent(id, pubKey, createdAt, tags, content, sig) @@ -842,7 +839,7 @@ class EventFactory { PublicationIndexEvent.KIND -> PublicationIndexEvent(id, pubKey, createdAt, tags, content, sig) PublicationContentEvent.KIND -> PublicationContentEvent(id, pubKey, createdAt, tags, content, sig) RelayReviewEvent.KIND -> RelayReviewEvent(id, pubKey, createdAt, tags, content, sig) - ContactCardEvent.KIND -> ContactCardEvent(id, pubKey, createdAt, tags, content, sig) + UserAssertionEvent.KIND -> UserAssertionEvent(id, pubKey, createdAt, tags, content, sig) EventAssertionEvent.KIND -> EventAssertionEvent(id, pubKey, createdAt, tags, content, sig) AddressableAssertionEvent.KIND -> AddressableAssertionEvent(id, pubKey, createdAt, tags, content, sig) ExternalIdAssertionEvent.KIND -> ExternalIdAssertionEvent(id, pubKey, createdAt, tags, content, sig) @@ -875,24 +872,23 @@ class EventFactory { ControlEditionEvent.KIND -> ControlEditionEvent(id, pubKey, createdAt, tags, content, sig) ConcordInviteListEvent.KIND -> ConcordInviteListEvent(id, pubKey, createdAt, tags, content, sig) ConcordInviteBundleEvent.KIND -> ConcordInviteBundleEvent(id, pubKey, createdAt, tags, content, sig) - SealedRumorEvent.KIND -> SealedRumorEvent(id, pubKey, createdAt, tags, content, sig) + SealEvent.KIND -> SealEvent(id, pubKey, createdAt, tags, content, sig) SearchRelayListEvent.KIND -> SearchRelayListEvent(id, pubKey, createdAt, tags, content, sig) SimpleGroupListEvent.KIND -> SimpleGroupListEvent(id, pubKey, createdAt, tags, content, sig) SoftwareApplicationEvent.KIND -> SoftwareApplicationEvent(id, pubKey, createdAt, tags, content, sig) SoftwareAssetEvent.KIND -> SoftwareAssetEvent(id, pubKey, createdAt, tags, content, sig) StallEvent.KIND -> StallEvent(id, pubKey, createdAt, tags, content, sig) - StatusEvent.KIND -> StatusEvent(id, pubKey, createdAt, tags, content, sig) + UserStatusEvent.KIND -> UserStatusEvent(id, pubKey, createdAt, tags, content, sig) TextNoteEvent.KIND -> TextNoteEvent(id, pubKey, createdAt, tags, content, sig) ThreadEvent.KIND -> ThreadEvent(id, pubKey, createdAt, tags, content, sig) TextNoteModificationEvent.KIND -> TextNoteModificationEvent(id, pubKey, createdAt, tags, content, sig) - TokenEvent.KIND -> TokenEvent(id, pubKey, createdAt, tags, content, sig) TorrentEvent.KIND -> TorrentEvent(id, pubKey, createdAt, tags, content, sig) TorrentCommentEvent.KIND -> TorrentCommentEvent(id, pubKey, createdAt, tags, content, sig) TrustedRelayListEvent.KIND -> TrustedRelayListEvent(id, pubKey, createdAt, tags, content, sig) TrustProviderListEvent.KIND -> TrustProviderListEvent(id, pubKey, createdAt, tags, content, sig) VideoCurationSetEvent.KIND -> VideoCurationSetEvent(id, pubKey, createdAt, tags, content, sig) - VideoHorizontalEvent.KIND -> VideoHorizontalEvent(id, pubKey, createdAt, tags, content, sig) - VideoVerticalEvent.KIND -> VideoVerticalEvent(id, pubKey, createdAt, tags, content, sig) + AddressableNormalVideoEvent.KIND -> AddressableNormalVideoEvent(id, pubKey, createdAt, tags, content, sig) + AddressableShortVideoEvent.KIND -> AddressableShortVideoEvent(id, pubKey, createdAt, tags, content, sig) TextTrackEvent.KIND -> TextTrackEvent(id, pubKey, createdAt, tags, content, sig) VideoCollaborationEvent.KIND -> VideoCollaborationEvent(id, pubKey, createdAt, tags, content, sig) VideoNormalEvent.KIND -> VideoNormalEvent(id, pubKey, createdAt, tags, content, sig) @@ -901,7 +897,7 @@ class EventFactory { VoiceReplyEvent.KIND -> VoiceReplyEvent(id, pubKey, createdAt, tags, content, sig) WakeUpEvent.KIND -> WakeUpEvent(id, pubKey, createdAt, tags, content, sig) WebBookmarkEvent.KIND -> WebBookmarkEvent(id, pubKey, createdAt, tags, content, sig) - WikiNoteEvent.KIND -> WikiNoteEvent(id, pubKey, createdAt, tags, content, sig) + WikiArticleEvent.KIND -> WikiArticleEvent(id, pubKey, createdAt, tags, content, sig) WorkoutRecordEvent.KIND -> WorkoutRecordEvent(id, pubKey, createdAt, tags, content, sig) ExerciseTemplateEvent.KIND -> ExerciseTemplateEvent(id, pubKey, createdAt, tags, content, sig) else -> factories[kind]?.build(id, pubKey, createdAt, tags, content, sig) ?: Event(id, pubKey, createdAt, kind, tags, content, sig) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/moderation/ModerationUnbanEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/moderation/ModerationUnbanEventTest.kt index 0f53a118c3..ec1a31dea2 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/moderation/ModerationUnbanEventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/buzz/moderation/ModerationUnbanEventTest.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.buzz.moderation -import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent +import com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent import kotlin.test.Test import kotlin.test.assertEquals @@ -40,7 +40,7 @@ class ModerationUnbanEventTest { @Test fun kindCollidesWithNip75GoalEvent() { // Documents the collision: this kind must NOT be registered in EventFactory, - // where 9041 already belongs to the NIP-75 GoalEvent. - assertEquals(GoalEvent.KIND, ModerationUnbanEvent.KIND) + // where 9041 already belongs to the NIP-75 ZapGoalEvent. + assertEquals(ZapGoalEvent.KIND, ModerationUnbanEvent.KIND) } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/graperank/GrapeRankPublisherTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/graperank/GrapeRankPublisherTest.kt index a5ef8d76eb..241e13ed53 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/graperank/GrapeRankPublisherTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/graperank/GrapeRankPublisherTest.kt @@ -27,8 +27,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.store.IEventStore import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventStore -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent -import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent import kotlinx.coroutines.runBlocking import kotlin.test.Test import kotlin.test.assertEquals @@ -48,8 +48,8 @@ class GrapeRankPublisherTest { provider: String, ): Map<String, Int?> = store - .query<Event>(Filter(kinds = listOf(ContactCardEvent.KIND), authors = listOf(provider))) - .filterIsInstance<ContactCardEvent>() + .query<Event>(Filter(kinds = listOf(UserAssertionEvent.KIND), authors = listOf(provider))) + .filterIsInstance<UserAssertionEvent>() .associate { it.aboutUser() to it.rank() } @Test @@ -72,7 +72,7 @@ class GrapeRankPublisherTest { assertEquals(mapOf(a to 50, b to 10), cardsByTarget(store, provider)) val firstIds = store - .query<Event>(Filter(kinds = listOf(ContactCardEvent.KIND), authors = listOf(provider))) + .query<Event>(Filter(kinds = listOf(UserAssertionEvent.KIND), authors = listOf(provider))) .map { it.id } .toSet() @@ -83,7 +83,7 @@ class GrapeRankPublisherTest { assertEquals(0, r2.retracted) val secondIds = store - .query<Event>(Filter(kinds = listOf(ContactCardEvent.KIND), authors = listOf(provider))) + .query<Event>(Filter(kinds = listOf(UserAssertionEvent.KIND), authors = listOf(provider))) .map { it.id } .toSet() assertEquals(firstIds, secondIds) @@ -98,7 +98,7 @@ class GrapeRankPublisherTest { assertEquals(mapOf(a to 60, c to 5), cardsByTarget(store, provider)) // …but its kind:5 tombstone remains, ready to sync to relays. - val deletions = store.query<Event>(Filter(kinds = listOf(DeletionEvent.KIND), authors = listOf(provider))) + val deletions = store.query<Event>(Filter(kinds = listOf(DeletionRequestEvent.KIND), authors = listOf(provider))) assertEquals(1, deletions.size) store.close() @@ -137,10 +137,10 @@ class GrapeRankPublisherTest { val a = hexKey(0xA) - suspend fun cardFor(target: String): ContactCardEvent = + suspend fun cardFor(target: String): UserAssertionEvent = store - .query<Event>(Filter(kinds = listOf(ContactCardEvent.KIND), authors = listOf(provider))) - .filterIsInstance<ContactCardEvent>() + .query<Event>(Filter(kinds = listOf(UserAssertionEvent.KIND), authors = listOf(provider))) + .filterIsInstance<UserAssertionEvent>() .first { it.aboutUser() == target } // A card carrying rank + followers + hops persists all three tags. diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/SoftwareApplicationEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/SoftwareApplicationEventTest.kt index c92f07fb5a..25990b13ff 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/SoftwareApplicationEventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/SoftwareApplicationEventTest.kt @@ -32,15 +32,17 @@ import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAss import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.apkCertificateHash import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.platform import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.versionCode -import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.SoftwareReleaseEvent import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.asSoftwareRelease import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.isNip82SoftwareRelease import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.shared.Platform import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtag +import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent +import com.vitorpamplona.quartz.utils.EventFactory import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -107,7 +109,7 @@ class SoftwareApplicationEventTest { id = "0".repeat(64), pubKey = "1".repeat(64), createdAt = 0, - kind = SoftwareReleaseEvent.KIND, + kind = ReleaseArtifactSetEvent.KIND, tags = arrayOf( arrayOf("d", "com.example.app@1.0.0"), @@ -126,7 +128,7 @@ class SoftwareApplicationEventTest { id = "0".repeat(64), pubKey = "1".repeat(64), createdAt = 0, - kind = SoftwareReleaseEvent.KIND, + kind = ReleaseArtifactSetEvent.KIND, tags = arrayOf(arrayOf("d", "some-uuid"), arrayOf("title", "My Release")), content = "", sig = "", @@ -141,7 +143,7 @@ class SoftwareApplicationEventTest { id = "0".repeat(64), pubKey = "1".repeat(64), createdAt = 0, - kind = SoftwareReleaseEvent.KIND, + kind = ReleaseArtifactSetEvent.KIND, tags = arrayOf( arrayOf("d", "com.example.app@1.0.0"), @@ -158,6 +160,53 @@ class SoftwareApplicationEventTest { assertEquals("main", release.channel()) } + @Test + fun eventFactory_parsesNip82ReleaseAsReleaseArtifactSet() { + val event = + EventFactory.create<Event>( + id = "0".repeat(64), + pubKey = "1".repeat(64), + createdAt = 0, + kind = ReleaseArtifactSetEvent.KIND, + tags = + arrayOf( + arrayOf("d", "com.example.app@1.0.0"), + arrayOf("i", "com.example.app"), + arrayOf("version", "1.0.0"), + arrayOf("c", "main"), + arrayOf("e", "2".repeat(64)), + ), + content = "Initial release", + sig = "", + ) + val release = assertIs<ReleaseArtifactSetEvent>(event) + assertTrue(release.isNip82SoftwareRelease()) + assertEquals("com.example.app", release.appId()) + assertEquals("1.0.0", release.version()) + assertEquals("main", release.channel()) + assertEquals(1, release.assets().size) + assertEquals("Initial release", release.releaseNotes()) + } + + @Test + fun buildSoftwareRelease_roundTripsNip82Fields() { + val template = + ReleaseArtifactSetEvent.buildSoftwareRelease( + appId = "com.example.app", + version = "2.0.0", + channel = "beta", + assets = emptyList(), + releaseNotes = "notes", + ) + assertEquals(ReleaseArtifactSetEvent.KIND, template.kind) + val tagsByName = template.tags.associate { it[0] to it.getOrNull(1) } + assertEquals("com.example.app@2.0.0", tagsByName["d"]) + assertEquals("com.example.app", tagsByName["i"]) + assertEquals("2.0.0", tagsByName["version"]) + assertEquals("beta", tagsByName["c"]) + assertEquals("notes", template.content) + } + @Test fun parses_realWorldAmethystEvent() { // Real NIP-82 event published for Amethyst — used as a regression fixture so renderer diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip85TrustedAssertions/AssertionEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip85TrustedAssertions/AssertionEventTest.kt index 7eccdaf3dd..bc225b0cb9 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip85TrustedAssertions/AssertionEventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip85TrustedAssertions/AssertionEventTest.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.experimental.nip85TrustedAssertions import com.vitorpamplona.quartz.nip85TrustedAssertions.addressables.AddressableAssertionEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.events.EventAssertionEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.externalIds.ExternalIdAssertionEvent -import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull @@ -36,7 +36,7 @@ class AssertionEventTest { fun parseUserAssertionWithAllTags() { val targetUser = "e88a691e98d9987c964521dff60025f60700378a4879180dcbbb4a5027850411" val event = - ContactCardEvent( + UserAssertionEvent( id = "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234", pubKey = servicePubKey, createdAt = 1700000000, @@ -89,7 +89,7 @@ class AssertionEventTest { @Test fun parseUserAssertionMinimalTags() { val event = - ContactCardEvent( + UserAssertionEvent( id = "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234", pubKey = servicePubKey, createdAt = 1700000000, @@ -228,7 +228,7 @@ class AssertionEventTest { @Test fun eventKindsAreCorrect() { - assertEquals(30382, ContactCardEvent.KIND) + assertEquals(30382, UserAssertionEvent.KIND) assertEquals(30383, EventAssertionEvent.KIND) assertEquals(30384, AddressableAssertionEvent.KIND) assertEquals(30385, ExternalIdAssertionEvent.KIND) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip85TrustedAssertions/ContactCardPetNameTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip85TrustedAssertions/ContactCardPetNameTest.kt index 17d1f2d14b..1f58771e64 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip85TrustedAssertions/ContactCardPetNameTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip85TrustedAssertions/ContactCardPetNameTest.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.experimental.nip85TrustedAssertions import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag -import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.PetNameTag import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.SummaryTag import kotlinx.coroutines.test.runTest @@ -36,15 +36,15 @@ class ContactCardPetNameTest { val signer = NostrSignerInternal(KeyPair()) val targetUser = "e88a691e98d9987c964521dff60025f60700378a4879180dcbbb4a5027850411" - private suspend fun ContactCardEvent.privatePetName() = privateTags(signer)?.firstNotNullOfOrNull(PetNameTag::parse) + private suspend fun UserAssertionEvent.privatePetName() = privateTags(signer)?.firstNotNullOfOrNull(PetNameTag::parse) - private suspend fun ContactCardEvent.privateSummary() = privateTags(signer)?.firstNotNullOfOrNull(SummaryTag::parse) + private suspend fun UserAssertionEvent.privateSummary() = privateTags(signer)?.firstNotNullOfOrNull(SummaryTag::parse) @Test fun createKeepsPetNameAndSummaryEncrypted() = runTest { val card = - ContactCardEvent.create( + UserAssertionEvent.create( targetUser = targetUser, petName = "Bob from work", summary = "Met at the conference", @@ -66,7 +66,7 @@ class ContactCardPetNameTest { fun updateReplacesPetNameAndKeepsOtherPrivateTags() = runTest { val card = - ContactCardEvent.create( + UserAssertionEvent.create( targetUser = targetUser, petName = "Bob", summary = "old summary", @@ -77,7 +77,7 @@ class ContactCardPetNameTest { val updated = signer.sign( - ContactCardEvent.updatePetNameAndSummary( + UserAssertionEvent.updatePetNameAndSummary( earlierVersion = card, petName = "Bobby", summary = "new summary", @@ -102,7 +102,7 @@ class ContactCardPetNameTest { fun updateWithNullsClearsBothFields() = runTest { val card = - ContactCardEvent.create( + UserAssertionEvent.create( targetUser = targetUser, petName = "Bob", summary = "summary", @@ -111,7 +111,7 @@ class ContactCardPetNameTest { val cleared = signer.sign( - ContactCardEvent.updatePetNameAndSummary( + UserAssertionEvent.updatePetNameAndSummary( earlierVersion = card, petName = null, summary = null, @@ -130,7 +130,7 @@ class ContactCardPetNameTest { val newEmoji = EmojiUrlTag("soapbox", "https://new.example/soapbox.png") val card = - ContactCardEvent.create( + UserAssertionEvent.create( targetUser = targetUser, petName = "Bob :wave:", emojis = listOf(oldEmoji), @@ -140,7 +140,7 @@ class ContactCardPetNameTest { val updated = signer.sign( - ContactCardEvent.updatePetNameAndSummary( + UserAssertionEvent.updatePetNameAndSummary( earlierVersion = card, petName = "Bob :soapbox:", emojis = listOf(newEmoji), @@ -159,7 +159,7 @@ class ContactCardPetNameTest { runTest { // a card that (incorrectly) carries public petname/summary tags val card = - ContactCardEvent.create( + UserAssertionEvent.create( targetUser = targetUser, signer = signer, publicInitializer = { @@ -171,7 +171,7 @@ class ContactCardPetNameTest { val updated = signer.sign( - ContactCardEvent.updatePetNameAndSummary( + UserAssertionEvent.updatePetNameAndSummary( earlierVersion = card, petName = "private bob", signer = signer, diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/cache/projection/EventStoreProjectionTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/cache/projection/EventStoreProjectionTest.kt index d16e03b68f..be617307b3 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/cache/projection/EventStoreProjectionTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/cache/projection/EventStoreProjectionTest.kt @@ -29,9 +29,9 @@ import com.vitorpamplona.quartz.nip01Core.store.ObservableEventStore import com.vitorpamplona.quartz.nip01Core.store.ObservableEventStore.StoreChange import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventStore import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtag -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -186,13 +186,13 @@ class EventStoreProjectionTest { fun addressableUpdateMutatesSlotInPlace() = runBlocking { val time = TimeUtils.now() - val v1 = signer.sign(LongTextNoteEvent.build("blog v1", "title", dTag = "blog", createdAt = time)) + val v1 = signer.sign(LongFormContentEvent.build("blog v1", "title", dTag = "blog", createdAt = time)) observable.insert(v1) val projection = - projectionOf<LongTextNoteEvent>( + projectionOf<LongFormContentEvent>( Filter( - kinds = listOf(LongTextNoteEvent.KIND), + kinds = listOf(LongFormContentEvent.KIND), authors = listOf(v1.pubKey), tags = mapOf("d" to listOf("blog")), ), @@ -201,7 +201,7 @@ class EventStoreProjectionTest { val seedList = projection.items val slot = seedList[0] - val v2 = signer.sign(LongTextNoteEvent.build("blog v2", "title", dTag = "blog", createdAt = time + 1)) + val v2 = signer.sign(LongFormContentEvent.build("blog v2", "title", dTag = "blog", createdAt = time + 1)) observable.insert(v2) awaitFlow(slot) { it.id == v2.id } @@ -220,7 +220,7 @@ class EventStoreProjectionTest { // v1 carries tag "nostr"; v2 changes the tag to "bitcoin". val v1 = signer.sign( - LongTextNoteEvent.build("blog v1", "title", dTag = "blog", createdAt = time) { + LongFormContentEvent.build("blog v1", "title", dTag = "blog", createdAt = time) { hashtag("nostr") }, ) @@ -228,9 +228,9 @@ class EventStoreProjectionTest { // Filter narrows to events that ALSO carry hashtag "nostr". val projection = - projectionOf<LongTextNoteEvent>( + projectionOf<LongFormContentEvent>( Filter( - kinds = listOf(LongTextNoteEvent.KIND), + kinds = listOf(LongFormContentEvent.KIND), authors = listOf(v1.pubKey), tags = mapOf("t" to listOf("nostr")), ), @@ -240,7 +240,7 @@ class EventStoreProjectionTest { val v2 = signer.sign( - LongTextNoteEvent.build("blog v2", "title", dTag = "blog", createdAt = time + 1) { + LongFormContentEvent.build("blog v2", "title", dTag = "blog", createdAt = time + 1) { hashtag("bitcoin") }, ) @@ -300,7 +300,7 @@ class EventStoreProjectionTest { projection.awaitLoaded() assertEquals(2, projection.items.size) - val deletion = signer.sign(DeletionEvent.build(listOf(a))) + val deletion = signer.sign(DeletionRequestEvent.build(listOf(a))) observable.insert(deletion) val after = projection.awaitItems { it.size == 1 } @@ -322,7 +322,7 @@ class EventStoreProjectionTest { val seed = projection.items assertEquals(1, seed.size) - val foreignDeletion = otherSigner.sign(DeletionEvent.build(listOf(a))) + val foreignDeletion = otherSigner.sign(DeletionRequestEvent.build(listOf(a))) observable.insert(foreignDeletion) // Give the projection time to process the event. diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/AddressableTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/AddressableTest.kt index 759d1b223b..8c26a66714 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/AddressableTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/AddressableTest.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite import androidx.sqlite.SQLiteException import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.test.Test import kotlin.test.assertEquals @@ -36,9 +36,9 @@ class AddressableTest : BaseDBTest() { fun testReplacingAddressables() = forEachDB { db -> val time = TimeUtils.now() - val version1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time)) - val version2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1)) - val version3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2)) + val version1 = signer.sign(LongFormContentEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time)) + val version2 = signer.sign(LongFormContentEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1)) + val version3 = signer.sign(LongFormContentEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2)) val addressableQuery = Filter(kinds = listOf(version1.kind), authors = listOf(version1.pubKey), tags = mapOf("d" to listOf(version1.dTag()))) @@ -65,9 +65,9 @@ class AddressableTest : BaseDBTest() { fun testBlockingOldAddressables() = forEachDB { db -> val time = TimeUtils.now() - val version1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time)) - val version2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1)) - val version3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2)) + val version1 = signer.sign(LongFormContentEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time)) + val version2 = signer.sign(LongFormContentEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1)) + val version3 = signer.sign(LongFormContentEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2)) val addressableQuery = Filter(kinds = listOf(version1.kind), authors = listOf(version1.pubKey), tags = mapOf("d" to listOf(version1.dTag()))) @@ -97,7 +97,7 @@ class AddressableTest : BaseDBTest() { // different content -> different ids. NIP-01 says the lower-id wins. val a = signer.sign( - LongTextNoteEvent.build( + LongFormContentEvent.build( "version A", "title", dTag = "tie", @@ -106,7 +106,7 @@ class AddressableTest : BaseDBTest() { ) val b = signer.sign( - LongTextNoteEvent.build( + LongFormContentEvent.build( "version B", "title", dTag = "tie", @@ -129,7 +129,7 @@ class AddressableTest : BaseDBTest() { val time = TimeUtils.now() val a = signer.sign( - LongTextNoteEvent.build( + LongFormContentEvent.build( "version A", "title", dTag = "tie", @@ -138,7 +138,7 @@ class AddressableTest : BaseDBTest() { ) val b = signer.sign( - LongTextNoteEvent.build( + LongFormContentEvent.build( "version B", "title", dTag = "tie", diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/AuthorsMissingOutboxTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/AuthorsMissingOutboxTest.kt index 103eb82d3c..d70bdd6952 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/AuthorsMissingOutboxTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/AuthorsMissingOutboxTest.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent @@ -83,7 +83,7 @@ class AuthorsMissingOutboxTest : BaseDBTest() { // NIP-09: the author deletes their own relay list. No 10002 row // remains, so the anti-join reports them as missing again. - db.insert(signer.sign(DeletionEvent.build(listOf(relayList)))) + db.insert(signer.sign(DeletionRequestEvent.build(listOf(relayList)))) assertEquals(setOf(signer.pubKey), db.authorsMissingOutbox().toSet()) } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/BasicTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/BasicTest.kt index 6c695ff744..bff21ff649 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/BasicTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/BasicTest.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtag import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import kotlin.test.Test @@ -261,7 +261,7 @@ class BasicTest : BaseDBTest() { forEachDB { db -> // Pre-load a target event and a deletion that blocks re-insertion. val deletedTarget = signer.sign(TextNoteEvent.build("target", createdAt = 1)) - val deletion = signer.sign(DeletionEvent.build(listOf(deletedTarget), createdAt = 100)) + val deletion = signer.sign(DeletionRequestEvent.build(listOf(deletedTarget), createdAt = 100)) db.insert(deletion) db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt index 811e9b5323..62ab2e4183 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt @@ -24,9 +24,9 @@ import androidx.sqlite.SQLiteException import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.test.Test @@ -51,7 +51,7 @@ class DeletionTest : BaseDBTest() { db.assertQuery(note2, Filter(ids = listOf(note2.id))) db.assertQuery(note3, Filter(ids = listOf(note3.id))) - val deletion = signer.sign(DeletionEvent.build(listOf(note1))) + val deletion = signer.sign(DeletionRequestEvent.build(listOf(note1))) db.insert(deletion) @@ -74,9 +74,9 @@ class DeletionTest : BaseDBTest() { fun testInsertDeleteEventOfAddressable() = forEachDB { db -> val time = TimeUtils.now() - val note1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time)) - val note2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1)) - val note3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2)) + val note1 = signer.sign(LongFormContentEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time)) + val note2 = signer.sign(LongFormContentEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1)) + val note3 = signer.sign(LongFormContentEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2)) db.insert(note1) @@ -95,7 +95,7 @@ class DeletionTest : BaseDBTest() { // Deletion must be timestamped >= note3.createdAt for it to // remove the latest addressable per NIP-09. - val deletion = signer.sign(DeletionEvent.build(listOf(note1), createdAt = time + 100)) + val deletion = signer.sign(DeletionRequestEvent.build(listOf(note1), createdAt = time + 100)) db.insert(deletion) @@ -118,9 +118,9 @@ class DeletionTest : BaseDBTest() { fun testInsertDeleteEventOfAddressable2() = forEachDB { db -> val time = TimeUtils.now() - val note1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time)) - val note2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1)) - val note3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2)) + val note1 = signer.sign(LongFormContentEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time)) + val note2 = signer.sign(LongFormContentEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1)) + val note3 = signer.sign(LongFormContentEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2)) db.insert(note1) db.insert(note2) @@ -130,7 +130,7 @@ class DeletionTest : BaseDBTest() { db.assertQuery(null, Filter(ids = listOf(note2.id))) db.assertQuery(note3, Filter(ids = listOf(note3.id))) - val deletion = signer.sign(DeletionEvent.buildAddressOnly(listOf(note1), createdAt = time + 100)) + val deletion = signer.sign(DeletionRequestEvent.buildAddressOnly(listOf(note1), createdAt = time + 100)) db.insert(deletion) @@ -165,7 +165,7 @@ class DeletionTest : BaseDBTest() { db.assertQuery(wrap1, Filter(ids = listOf(wrap1.id))) db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id))) - val randomDeletionToWrap = signer.sign(DeletionEvent.build(listOf(wrap1))) + val randomDeletionToWrap = signer.sign(DeletionRequestEvent.build(listOf(wrap1))) db.insert(randomDeletionToWrap) @@ -173,7 +173,7 @@ class DeletionTest : BaseDBTest() { db.assertQuery(wrap1, Filter(ids = listOf(wrap1.id))) db.assertQuery(wrap2, Filter(ids = listOf(wrap2.id))) - val deletion = me.sign(DeletionEvent.build(listOf(wrap1))) + val deletion = me.sign(DeletionRequestEvent.build(listOf(wrap1))) db.insert(deletion) @@ -427,14 +427,14 @@ class DeletionTest : BaseDBTest() { // A deletion event from a *different* author must not remove the // target — only the original author can delete via NIP-09. - val strangerDeletion = stranger.sign(DeletionEvent.build(listOf(target))) + val strangerDeletion = stranger.sign(DeletionRequestEvent.build(listOf(target))) db.insert(strangerDeletion) db.assertQuery(target, Filter(ids = listOf(target.id))) db.assertQuery(strangerDeletion, Filter(ids = listOf(strangerDeletion.id))) // The owner's deletion still works after a failed third-party attempt. - val ownerDeletion = owner.sign(DeletionEvent.build(listOf(target))) + val ownerDeletion = owner.sign(DeletionRequestEvent.build(listOf(target))) db.insert(ownerDeletion) db.assertQuery(null, Filter(ids = listOf(target.id))) } @@ -447,7 +447,7 @@ class DeletionTest : BaseDBTest() { // a second deletion that removes the first. Cross-author // deletion of a kind-5 must NOT work. val target = signer.sign(TextNoteEvent.build("target")) - val firstDeletion = signer.sign(DeletionEvent.build(listOf(target), createdAt = 100)) + val firstDeletion = signer.sign(DeletionRequestEvent.build(listOf(target), createdAt = 100)) db.insert(target) db.insert(firstDeletion) @@ -457,13 +457,13 @@ class DeletionTest : BaseDBTest() { // Stranger cannot delete the kind-5. val stranger = NostrSignerSync() val strangerDeletion = - stranger.sign(DeletionEvent.build(listOf(firstDeletion), createdAt = 200)) + stranger.sign(DeletionRequestEvent.build(listOf(firstDeletion), createdAt = 200)) db.insert(strangerDeletion) db.assertQuery(firstDeletion, Filter(ids = listOf(firstDeletion.id))) // Same author can delete their previous kind-5. val secondDeletion = - signer.sign(DeletionEvent.build(listOf(firstDeletion), createdAt = 300)) + signer.sign(DeletionRequestEvent.build(listOf(firstDeletion), createdAt = 300)) db.insert(secondDeletion) db.assertQuery(null, Filter(ids = listOf(firstDeletion.id))) db.assertQuery(secondDeletion, Filter(ids = listOf(secondDeletion.id))) @@ -493,17 +493,17 @@ class DeletionTest : BaseDBTest() { // Inner author (the "sender") cannot delete a wrap addressed to // someone else — sender pubkey != wrap's pubkey_owner_hash. - val senderDeletion = sender.sign(DeletionEvent.build(listOf(wrap))) + val senderDeletion = sender.sign(DeletionRequestEvent.build(listOf(wrap))) db.insert(senderDeletion) db.assertQuery(wrap, Filter(ids = listOf(wrap.id))) // An unrelated third party can't delete it either. - val unrelatedDeletion = unrelated.sign(DeletionEvent.build(listOf(wrap))) + val unrelatedDeletion = unrelated.sign(DeletionRequestEvent.build(listOf(wrap))) db.insert(unrelatedDeletion) db.assertQuery(wrap, Filter(ids = listOf(wrap.id))) // The recipient (whose pubkey matches pubkey_owner_hash) can. - val recipientDeletion = recipient.sign(DeletionEvent.build(listOf(wrap))) + val recipientDeletion = recipient.sign(DeletionRequestEvent.build(listOf(wrap))) db.insert(recipientDeletion) db.assertQuery(null, Filter(ids = listOf(wrap.id))) } @@ -514,7 +514,7 @@ class DeletionTest : BaseDBTest() { // Old addressable (will be deleted by deletion request) val old = signer.sign( - LongTextNoteEvent.build( + LongFormContentEvent.build( "version 1", "title", dTag = "blog-1", @@ -528,7 +528,7 @@ class DeletionTest : BaseDBTest() { // Deletion targeting the address with created_at = 1500 val deletion = signer.sign( - DeletionEvent.buildAddressOnly( + DeletionRequestEvent.buildAddressOnly( listOf(old), createdAt = 1500, ), @@ -542,7 +542,7 @@ class DeletionTest : BaseDBTest() { // created_at AFTER the deletion. NIP-09 says it must be kept. val newer = signer.sign( - LongTextNoteEvent.build( + LongFormContentEvent.build( "version 2", "title", dTag = "blog-1", diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/InsertOutcomeClassificationTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/InsertOutcomeClassificationTest.kt index 6507de40f4..2bdcba4dc0 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/InsertOutcomeClassificationTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/InsertOutcomeClassificationTest.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.store.IEventStore import com.vitorpamplona.quartz.nip01Core.store.RejectionReason import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.test.Test import kotlin.test.assertEquals @@ -111,8 +111,8 @@ class InsertOutcomeClassificationTest : BaseDBTest() { fun olderAddressableIsRejectedAsSuperseded() = forEachDB { db -> val time = TimeUtils.now() - val older = signer.sign(LongTextNoteEvent.build("v1", "title", dTag = "blog", createdAt = time)) - val newer = signer.sign(LongTextNoteEvent.build("v2", "title", dTag = "blog", createdAt = time + 1)) + val older = signer.sign(LongFormContentEvent.build("v1", "title", dTag = "blog", createdAt = time)) + val newer = signer.sign(LongFormContentEvent.build("v2", "title", dTag = "blog", createdAt = time + 1)) assertEquals(IEventStore.InsertOutcome.Accepted, db.batchInsert(listOf<Event>(newer))[0]) assertRejected(RejectionReason.SUPERSEDED, db.batchInsert(listOf<Event>(older))[0]) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt index ac034e6af3..496def0887 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent -import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent import kotlin.test.Test import kotlin.test.assertEquals @@ -984,7 +984,7 @@ class QueryAssemblerTest : BaseDBTest() { forEachDB { db -> val filter = Filter( - kinds = listOf(ContactCardEvent.KIND), + kinds = listOf(UserAssertionEvent.KIND), authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), tags = mapOf( diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SearchTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SearchTest.kt index 7f238e30e6..23024b8be5 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SearchTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SearchTest.kt @@ -29,10 +29,10 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.test.Test @@ -134,7 +134,7 @@ class SearchTest : BaseDBTest() { val time = TimeUtils.now() val v1 = signer.sign( - LongTextNoteEvent.build( + LongFormContentEvent.build( "first version uniqalpha", title = "blog title", dTag = "fts-rotation", @@ -143,7 +143,7 @@ class SearchTest : BaseDBTest() { ) val v2 = signer.sign( - LongTextNoteEvent.build( + LongFormContentEvent.build( "second version uniqbeta", title = "blog title", dTag = "fts-rotation", @@ -163,10 +163,10 @@ class SearchTest : BaseDBTest() { @Test fun testNewlySearchableKinds() = forEachDB { db -> - // CalendarEvent indexes the title tag plus the free-text content. + // CalendarCollectionEvent indexes the title tag plus the free-text content. val cal = signer.sign( - CalendarEvent.build( + CalendarCollectionEvent.build( title = "uniqtitle Meetup", content = "annual uniqbody gathering", ), @@ -254,7 +254,7 @@ class SearchTest : BaseDBTest() { // events that were stored before their kind became searchable. val cal = signer.sign( - CalendarEvent.build( + CalendarCollectionEvent.build( title = "uniqtitle Meetup", content = "annual uniqbody gathering", ), diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/ChannelSettingsTagTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/ChannelSettingsTagTest.kt index 080e477790..6f0f7e4b28 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/ChannelSettingsTagTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/ChannelSettingsTagTest.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip29RelayGroups import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.EditMetadataEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupEditMetadataEvent import com.vitorpamplona.quartz.utils.EventFactory import kotlin.test.Test import kotlin.test.assertEquals @@ -48,22 +48,22 @@ class ChannelSettingsTagTest { @Test fun editEmitsVisibilityTagOnlyWhenSet() { - val priv = EditMetadataEvent.build(gid, visibility = "private") + val priv = GroupEditMetadataEvent.build(gid, visibility = "private") assertEquals("private", tagValue(priv.tags, "visibility")) - val open = EditMetadataEvent.build(gid, visibility = "open") + val open = GroupEditMetadataEvent.build(gid, visibility = "open") assertEquals("open", tagValue(open.tags, "visibility")) // Absent by default so an ordinary metadata edit doesn't reclassify visibility. - assertNull(tagValue(EditMetadataEvent.build(gid, name = "x").tags, "visibility")) + assertNull(tagValue(GroupEditMetadataEvent.build(gid, name = "x").tags, "visibility")) } @Test fun editEmitsArchivedTagAsTrueFalse() { - assertEquals("true", tagValue(EditMetadataEvent.build(gid, archived = true).tags, "archived")) - assertEquals("false", tagValue(EditMetadataEvent.build(gid, archived = false).tags, "archived")) + assertEquals("true", tagValue(GroupEditMetadataEvent.build(gid, archived = true).tags, "archived")) + assertEquals("false", tagValue(GroupEditMetadataEvent.build(gid, archived = false).tags, "archived")) // Null archived means "don't touch it" — no tag emitted. - assertNull(tagValue(EditMetadataEvent.build(gid, name = "x").tags, "archived")) + assertNull(tagValue(GroupEditMetadataEvent.build(gid, name = "x").tags, "archived")) } @Test diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/Nip29ArmadaInteropTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/Nip29ArmadaInteropTest.kt index dae29d1b8c..6d7949047d 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/Nip29ArmadaInteropTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/Nip29ArmadaInteropTest.kt @@ -32,16 +32,16 @@ import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupAdminsEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMembersEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupParticipantsEvent -import com.vitorpamplona.quartz.nip29RelayGroups.metadata.SupportedRolesEvent +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupRolesEvent import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateGroupEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateInviteEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.DeleteEventEvent import com.vitorpamplona.quartz.nip29RelayGroups.moderation.DeleteGroupEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.EditMetadataEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.PutUserEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.RemoveUserEvent -import com.vitorpamplona.quartz.nip29RelayGroups.request.JoinRequestEvent -import com.vitorpamplona.quartz.nip29RelayGroups.request.LeaveRequestEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupCreateInviteEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupDeleteEventEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupEditMetadataEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupPutUserEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupRemoveUserEvent +import com.vitorpamplona.quartz.nip29RelayGroups.request.GroupJoinRequestEvent +import com.vitorpamplona.quartz.nip29RelayGroups.request.GroupLeaveRequestEvent import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.GroupTag import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent @@ -169,12 +169,12 @@ class Nip29ArmadaInteropTest { fun parsesGroupRoles() { val e = parse( - SupportedRolesEvent.KIND, + GroupRolesEvent.KIND, arrayOf( arrayOf("role", "admin", "Full control"), arrayOf("role", "moderator"), ), - ) as SupportedRolesEvent + ) as GroupRolesEvent val roles = e.roles() assertEquals("admin", roles[0].name) assertEquals("Full control", roles[0].description) @@ -202,37 +202,37 @@ class Nip29ArmadaInteropTest { assertTrue(parse(CreateGroupEvent.KIND, arrayOf(arrayOf("h", gid)), pubKey = alice) is CreateGroupEvent) assertTrue(parse(DeleteGroupEvent.KIND, arrayOf(arrayOf("h", gid)), pubKey = alice) is DeleteGroupEvent) - val put = parse(PutUserEvent.KIND, arrayOf(arrayOf("h", gid), arrayOf("p", bob, "moderator")), pubKey = alice) as PutUserEvent + val put = parse(GroupPutUserEvent.KIND, arrayOf(arrayOf("h", gid), arrayOf("p", bob, "moderator")), pubKey = alice) as GroupPutUserEvent assertEquals(gid, put.groupId()) assertEquals(listOf(bob), put.userPubKeys()) - val remove = parse(RemoveUserEvent.KIND, arrayOf(arrayOf("h", gid), arrayOf("p", bob)), content = "spam", pubKey = alice) as RemoveUserEvent + val remove = parse(GroupRemoveUserEvent.KIND, arrayOf(arrayOf("h", gid), arrayOf("p", bob)), content = "spam", pubKey = alice) as GroupRemoveUserEvent assertEquals(listOf(bob), remove.userPubKeys()) assertEquals("spam", remove.content) - val del = parse(DeleteEventEvent.KIND, arrayOf(arrayOf("h", gid), arrayOf("e", "de".repeat(32))), pubKey = alice) as DeleteEventEvent + val del = parse(GroupDeleteEventEvent.KIND, arrayOf(arrayOf("h", gid), arrayOf("e", "de".repeat(32))), pubKey = alice) as GroupDeleteEventEvent assertEquals(listOf("de".repeat(32)), del.deletedEventIds()) - val invite = parse(CreateInviteEvent.KIND, arrayOf(arrayOf("h", gid), arrayOf("code", "abc123")), pubKey = alice) as CreateInviteEvent + val invite = parse(GroupCreateInviteEvent.KIND, arrayOf(arrayOf("h", gid), arrayOf("code", "abc123")), pubKey = alice) as GroupCreateInviteEvent assertEquals("abc123", invite.code()) - val edit = parse(EditMetadataEvent.KIND, arrayOf(arrayOf("h", gid), arrayOf("name", "N"), arrayOf("public"), arrayOf("open")), pubKey = alice) as EditMetadataEvent + val edit = parse(GroupEditMetadataEvent.KIND, arrayOf(arrayOf("h", gid), arrayOf("name", "N"), arrayOf("public"), arrayOf("open")), pubKey = alice) as GroupEditMetadataEvent assertEquals("N", edit.name()) assertEquals(gid, edit.groupId()) } @Test fun parsesJoinAndLeaveRequests() { - val join = parse(JoinRequestEvent.KIND, arrayOf(arrayOf("h", gid), arrayOf("code", "inv1")), content = "let me in", pubKey = alice) as JoinRequestEvent + val join = parse(GroupJoinRequestEvent.KIND, arrayOf(arrayOf("h", gid), arrayOf("code", "inv1")), content = "let me in", pubKey = alice) as GroupJoinRequestEvent assertEquals(gid, join.groupId()) assertEquals("inv1", join.inviteCode()) assertEquals("let me in", join.content) // Armada also sends a join with no code. - val joinNoCode = parse(JoinRequestEvent.KIND, arrayOf(arrayOf("h", gid)), pubKey = alice) as JoinRequestEvent + val joinNoCode = parse(GroupJoinRequestEvent.KIND, arrayOf(arrayOf("h", gid)), pubKey = alice) as GroupJoinRequestEvent assertNull(joinNoCode.inviteCode()) - val leave = parse(LeaveRequestEvent.KIND, arrayOf(arrayOf("h", gid)), pubKey = alice) as LeaveRequestEvent + val leave = parse(GroupLeaveRequestEvent.KIND, arrayOf(arrayOf("h", gid)), pubKey = alice) as GroupLeaveRequestEvent assertEquals(gid, leave.groupId()) } @@ -399,7 +399,7 @@ class Nip29ArmadaInteropTest { @Test fun buildsEditMetadataWithTopicsAndGeohashes() { val edit = - EditMetadataEvent.build( + GroupEditMetadataEvent.build( groupId = gid, name = "Bitcoin Devs", hashtags = listOf("bitcoin"), @@ -412,12 +412,12 @@ class Nip29ArmadaInteropTest { @Test fun buildsCreateAndModerationEvents() { assertTrue(CreateGroupEvent.build(gid).tags.any { it[0] == "h" && it[1] == gid }) - assertTrue(CreateInviteEvent.build(gid, "code42").tags.any { it[0] == "code" && it[1] == "code42" }) + assertTrue(GroupCreateInviteEvent.build(gid, "code42").tags.any { it[0] == "code" && it[1] == "code42" }) - val put = PutUserEvent.build(gid, listOf(bob to listOf("moderator"))).tags + val put = GroupPutUserEvent.build(gid, listOf(bob to listOf("moderator"))).tags assertTrue(put.any { it[0] == "p" && it[1] == bob && it.contains("moderator") }) - val remove = RemoveUserEvent.build(gid, listOf(bob)).tags + val remove = GroupRemoveUserEvent.build(gid, listOf(bob)).tags assertTrue(remove.any { it[0] == "p" && it[1] == bob }) // relay29 forks are avoided by NOT emitting `previous` when none provided. @@ -466,12 +466,12 @@ class Nip29ArmadaInteropTest { @Test fun buildsJoinLeaveAndGroupScopedChat() { - val join = JoinRequestEvent.build(gid, reason = "hi", inviteCode = "inv9") + val join = GroupJoinRequestEvent.build(gid, reason = "hi", inviteCode = "inv9") assertEquals("hi", join.content) assertTrue(join.tags.any { it[0] == "h" && it[1] == gid }) assertTrue(join.tags.any { it[0] == "code" && it[1] == "inv9" }) - assertTrue(LeaveRequestEvent.build(gid).tags.any { it[0] == "h" && it[1] == gid }) + assertTrue(GroupLeaveRequestEvent.build(gid).tags.any { it[0] == "h" && it[1] == gid }) // Group-scope a plain kind-9 chat message via the shared `hTag` helper. val chat = ChatEvent.build("gm") { hTag(gid) } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/PinEventsTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/PinEventsTest.kt index b5ee8e80f3..27dea39abd 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/PinEventsTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/PinEventsTest.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.quartz.buzz.cwChannelWindow.ThreadSummaryContent import com.vitorpamplona.quartz.buzz.cwChannelWindow.ThreadSummaryEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupPinnedEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.UpdatePinListEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupUpdatePinListEvent import com.vitorpamplona.quartz.nip29RelayGroups.tags.GroupIdTag import com.vitorpamplona.quartz.utils.EventFactory import kotlin.test.Test @@ -90,19 +90,19 @@ class PinEventsTest { @Test fun updatePinListEventParsesGroupAndIds() { val tags = arrayOf(arrayOf("h", gid), arrayOf("e", id1), arrayOf("e", id2)) - val event: Event = EventFactory.create("00".repeat(32), relaySelf, 100, UpdatePinListEvent.KIND, tags, "", "22".repeat(64)) + val event: Event = EventFactory.create("00".repeat(32), relaySelf, 100, GroupUpdatePinListEvent.KIND, tags, "", "22".repeat(64)) - assertEquals(true, event is UpdatePinListEvent) - event as UpdatePinListEvent + assertEquals(true, event is GroupUpdatePinListEvent) + event as GroupUpdatePinListEvent assertEquals(gid, event.groupId()) assertEquals(listOf(id1, id2), event.pinnedEventIds()) } @Test fun updatePinListBuildCarriesHTagAndFullList() { - val template = UpdatePinListEvent.build(gid, listOf(id1, id2)) + val template = GroupUpdatePinListEvent.build(gid, listOf(id1, id2)) - assertEquals(UpdatePinListEvent.KIND, template.kind) + assertEquals(GroupUpdatePinListEvent.KIND, template.kind) assertEquals(gid, template.tags.firstOrNull { it[0] == GroupIdTag.TAG_NAME }?.getOrNull(1)) assertEquals(listOf(id1, id2), template.tags.filter { it[0] == "e" }.map { it[1] }) } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/SubgroupTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/SubgroupTest.kt index cc4360e828..58d16706f6 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/SubgroupTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/SubgroupTest.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip29RelayGroups import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.EditMetadataEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupEditMetadataEvent import com.vitorpamplona.quartz.utils.EventFactory import kotlin.test.Test import kotlin.test.assertEquals @@ -66,8 +66,8 @@ class SubgroupTest { @Test fun editMetadataRoundTripsParentAndChildren() { - val template = EditMetadataEvent.build("nostr", name = "Nostr", parent = "social", children = listOf("nip29")) - val event = EventFactory.create(id, relaySelf, template.createdAt, EditMetadataEvent.KIND, template.tags, "", sig) as EditMetadataEvent + val template = GroupEditMetadataEvent.build("nostr", name = "Nostr", parent = "social", children = listOf("nip29")) + val event = EventFactory.create(id, relaySelf, template.createdAt, GroupEditMetadataEvent.KIND, template.tags, "", sig) as GroupEditMetadataEvent assertEquals("nostr", event.groupId()) assertEquals("social", event.parent()) @@ -76,8 +76,8 @@ class SubgroupTest { @Test fun editMetadataWithoutParentRoots() { - val template = EditMetadataEvent.build("nostr", name = "Nostr") - val event = EventFactory.create(id, relaySelf, template.createdAt, EditMetadataEvent.KIND, template.tags, "", sig) as EditMetadataEvent + val template = GroupEditMetadataEvent.build("nostr", name = "Nostr") + val event = EventFactory.create(id, relaySelf, template.createdAt, GroupEditMetadataEvent.KIND, template.tags, "", sig) as GroupEditMetadataEvent assertNull(event.parent()) } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/BunkerRequestProcessorConcurrencyTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/BunkerRequestProcessorConcurrencyTest.kt index 7ee9fdc303..5226a302ce 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/BunkerRequestProcessorConcurrencyTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/BunkerRequestProcessorConcurrencyTest.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest @@ -101,7 +101,7 @@ class BunkerRequestProcessorConcurrencyTest { fromPublicKey: HexKey, ) = "" - override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent = throw NotImplementedError() + override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = throw NotImplementedError() override suspend fun deriveKey(nonce: HexKey): HexKey = throw NotImplementedError() diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/BunkerRequestProcessorTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/BunkerRequestProcessorTest.kt index 3533bb2959..bc5598a518 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/BunkerRequestProcessorTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/BunkerRequestProcessorTest.kt @@ -41,7 +41,7 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseGetRelays import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePong import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePublicKey import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals @@ -116,7 +116,7 @@ class BunkerRequestProcessorTest { return "nip44dec:$ciphertext" } - override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent = throw NotImplementedError() + override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = throw NotImplementedError() override suspend fun deriveKey(nonce: HexKey): HexKey = throw NotImplementedError() @@ -371,7 +371,7 @@ class BunkerRequestProcessorTest { fromPublicKey: HexKey, ) = "" - override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent = throw NotImplementedError() + override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = throw NotImplementedError() override suspend fun deriveKey(nonce: HexKey): HexKey = throw NotImplementedError() diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/NostrConnectSignerServiceTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/NostrConnectSignerServiceTest.kt index 3dcf86afd1..0e631bc3e1 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/NostrConnectSignerServiceTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/NostrConnectSignerServiceTest.kt @@ -39,7 +39,7 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEvent import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePublicKey import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.utils.RandomInstance import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -112,7 +112,7 @@ class NostrConnectSignerServiceTest { fromPublicKey: HexKey, ) = ciphertext - override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent = throw NotImplementedError() + override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = throw NotImplementedError() override suspend fun deriveKey(nonce: HexKey): HexKey = throw NotImplementedError() diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt index 4e9a209c09..edc3143ba5 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceMethod import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoMethod import kotlinx.coroutines.test.runTest @@ -44,7 +44,7 @@ class LnZapPaymentRequestNip44EventTest { val request = GetBalanceMethod.Companion.create() val event = - LnZapPaymentRequestEvent.Companion.createRequest( + NwcRequestEvent.Companion.createRequest( request = request, walletServicePubkey = walletServicePubkey, signer = clientSigner, @@ -68,7 +68,7 @@ class LnZapPaymentRequestNip44EventTest { val request = GetInfoMethod.Companion.create() val event = - LnZapPaymentRequestEvent.Companion.createRequest( + NwcRequestEvent.Companion.createRequest( request = request, walletServicePubkey = walletServicePubkey, signer = clientSigner, diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcOutgoingMetadataTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcOutgoingMetadataTest.kt index 8bddfc1446..c7020dc022 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcOutgoingMetadataTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcOutgoingMetadataTest.kt @@ -84,7 +84,7 @@ class NwcOutgoingMetadataTest { /** * THE INTEROP PROPERTY. NIP-57 sets a zap invoice's `description_hash` to the * sha256 of the raw JSON the LNURL callback received in `nostr=` — which is - * `LnZapRequestEvent.toJson()` (see LightningAddressResolver). A wallet that + * `ZapRequestEvent.toJson()` (see LightningAddressResolver). A wallet that * binds a stored zap request to the invoice it labels hashes the bytes of the * `nostr` member, so anything short of byte-identity reads as a forged event * and the row is silently stored unlabelled. diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcRefusalRoundTripTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcRefusalRoundTripTest.kt index e90ea7e6a2..9c69def4ca 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcRefusalRoundTripTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcRefusalRoundTripTest.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.quartz.nip47WalletConnect import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip47WalletConnect.cache.NostrWalletConnectResponseCache -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.rpc.IErrorResponseLike import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod import kotlinx.coroutines.test.runTest @@ -50,14 +50,14 @@ class NwcRefusalRoundTripTest { for (useNip44 in listOf(false, true)) { val request = - LnZapPaymentRequestEvent.createRequest( + NwcRequestEvent.createRequest( PayInvoiceMethod.create("lnbc50n1pjtest"), walletSigner.pubKey, clientSigner, useNip44 = useNip44, ) - // Encrypted by hand rather than through LnZapPaymentResponseEvent.createResponse, + // Encrypted by hand rather than through NwcResponseEvent.createResponse, // which re-serializes a Response: the point is the wallet's bytes, verbatim. val encrypted = if (useNip44) { @@ -66,9 +66,9 @@ class NwcRefusalRoundTripTest { walletSigner.nip04Encrypt(walletJson, clientSigner.pubKey) } val reply = - walletSigner.sign<LnZapPaymentResponseEvent>( + walletSigner.sign<NwcResponseEvent>( request.createdAt, - LnZapPaymentResponseEvent.KIND, + NwcResponseEvent.KIND, arrayOf(arrayOf("p", clientSigner.pubKey), arrayOf("e", request.id)), encrypted, ) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcRequestEventTest.kt similarity index 93% rename from quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEventTest.kt rename to quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcRequestEventTest.kt index 07fa5ac130..2b3f56f594 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcRequestEventTest.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal -import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceMethod import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceMethod import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod @@ -36,10 +36,10 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue -class LnZapPaymentRequestEventTest { +class NwcRequestEventTest { @Test fun testEventKind() { - assertEquals(23194, LnZapPaymentRequestEvent.KIND) + assertEquals(23194, NwcRequestEvent.KIND) } @Test @@ -52,7 +52,7 @@ class LnZapPaymentRequestEventTest { walletKeyPair.pubKey.toHexKey() val event = - LnZapPaymentRequestEvent.create( + NwcRequestEvent.create( lnInvoice = "lnbc50n1...", walletServicePubkey = walletServicePubkey, signer = clientSigner, @@ -77,7 +77,7 @@ class LnZapPaymentRequestEventTest { val request = GetBalanceMethod.create() val event = - LnZapPaymentRequestEvent.createRequest( + NwcRequestEvent.createRequest( request = request, walletServicePubkey = walletServicePubkey, signer = clientSigner, @@ -101,7 +101,7 @@ class LnZapPaymentRequestEventTest { walletKeyPair.pubKey.toHexKey() val event = - LnZapPaymentRequestEvent.create( + NwcRequestEvent.create( lnInvoice = "lnbc50n1...", walletServicePubkey = walletServicePubkey, signer = clientSigner, @@ -125,7 +125,7 @@ class LnZapPaymentRequestEventTest { val request = MakeInvoiceMethod.create(5000L, "test payment") val event = - LnZapPaymentRequestEvent.createRequest( + NwcRequestEvent.createRequest( request = request, walletServicePubkey = walletServicePubkey, signer = clientSigner, @@ -150,7 +150,7 @@ class LnZapPaymentRequestEventTest { walletKeyPair.pubKey.toHexKey() val event = - LnZapPaymentRequestEvent.create( + NwcRequestEvent.create( lnInvoice = "lnbc50n1...", walletServicePubkey = walletServicePubkey, signer = clientSigner, diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractorTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractorTest.kt index 3075095a39..5d044ecb52 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractorTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractorTest.kt @@ -33,15 +33,15 @@ import com.vitorpamplona.quartz.nip15Marketplace.product.ProductEvent import com.vitorpamplona.quartz.nip15Marketplace.stall.StallEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip29RelayGroups.moderation.EditMetadataEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent +import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupEditMetadataEvent import com.vitorpamplona.quartz.nip32Labeling.LabelEvent import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent import com.vitorpamplona.quartz.nip69P2pOrderEvents.P2POrderEvent -import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent +import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nipB0WebBookmarks.WebBookmarkEvent @@ -72,7 +72,7 @@ class SearchFieldExtractorTest { @Test fun longFormDecomposesIntoTitleSummaryHashtagsContent() { val tags = arrayOf(arrayOf("d", "post"), arrayOf("title", "My Post"), arrayOf("summary", "tl;dr"), arrayOf("t", "nostr"), arrayOf("t", "search")) - val fields = SearchFieldExtractor.extract(LongTextNoteEvent("2".repeat(64), alice, 1L, tags, "the whole article", "")) + val fields = SearchFieldExtractor.extract(LongFormContentEvent("2".repeat(64), alice, 1L, tags, "the whole article", "")) assertEquals(IndexableFields.Tiered(primary = listOf("My Post"), secondary = listOf("tl;dr"), text = "the whole article", hashtags = listOf("nostr", "search")), fields) } @@ -165,7 +165,7 @@ class SearchFieldExtractorTest { arrayOf("t", "bitcoin"), arrayOf("rank", "87"), ) - val fields = SearchFieldExtractor.extract(ContactCardEvent("f".repeat(64), alice, 1L, tags, "", "")) + val fields = SearchFieldExtractor.extract(UserAssertionEvent("f".repeat(64), alice, 1L, tags, "", "")) assertEquals( IndexableFields.Tiered( primary = listOf("Verified Human"), @@ -184,7 +184,7 @@ class SearchFieldExtractorTest { // role, once -- and a hashtags-only extraction must not normalize to // None (Tiered.isEmpty() compares against a fully-empty Tiered). val tags = arrayOf(arrayOf("d", alice), arrayOf("t", "bitcoin"), arrayOf("t", "nostr"), arrayOf("rank", "87")) - val fields = SearchFieldExtractor.extract(ContactCardEvent("2a".repeat(32), alice, 1L, tags, "encrypted", "")) + val fields = SearchFieldExtractor.extract(UserAssertionEvent("2a".repeat(32), alice, 1L, tags, "encrypted", "")) assertEquals(IndexableFields.Tiered(hashtags = listOf("bitcoin", "nostr")), fields) } @@ -194,7 +194,7 @@ class SearchFieldExtractorTest { // encrypted content, which is never indexed -- so a card carrying only // scores has nothing to search. val tags = arrayOf(arrayOf("d", alice), arrayOf("rank", "87"), arrayOf("followers", "1200")) - assertEquals(IndexableFields.None, SearchFieldExtractor.extract(ContactCardEvent("1a".repeat(32), alice, 1L, tags, "encrypted", ""))) + assertEquals(IndexableFields.None, SearchFieldExtractor.extract(UserAssertionEvent("1a".repeat(32), alice, 1L, tags, "encrypted", ""))) } @Test @@ -270,7 +270,7 @@ class SearchFieldExtractorTest { // kind 9002 edits what kind 39000 publishes; it was the only half of // the pair without a branch. Its hashtags() is `t`: carried once. val tags = arrayOf(arrayOf("h", "grp"), arrayOf("name", "Nostr Devs"), arrayOf("about", "we build"), arrayOf("t", "nostr")) - val fields = SearchFieldExtractor.extract(EditMetadataEvent("23".repeat(32), alice, 1L, tags, "", "")) + val fields = SearchFieldExtractor.extract(GroupEditMetadataEvent("23".repeat(32), alice, 1L, tags, "", "")) assertEquals( IndexableFields.Tiered(primary = listOf("Nostr Devs"), secondary = listOf("we build"), hashtags = listOf("nostr")), fields, diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/RelayFeedsListEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/FavoriteRelayListEventTest.kt similarity index 87% rename from quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/RelayFeedsListEventTest.kt rename to quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/FavoriteRelayListEventTest.kt index 40af5a2166..62e54bdc8f 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/RelayFeedsListEventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/FavoriteRelayListEventTest.kt @@ -28,7 +28,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -class RelayFeedsListEventTest { +class FavoriteRelayListEventTest { private val signer = NostrSignerInternal("nsec10g0wheggqn9dawlc0yuv6adnat6n09anr7eyykevw2dm8xa5fffs0wsdsr".nsecToKeyPair()) private val damus = RelayUrlNormalizer.normalize("wss://relay.damus.io") @@ -37,7 +37,7 @@ class RelayFeedsListEventTest { @Test fun kindMatchesSpec() { - assertEquals(10012, RelayFeedsListEvent.KIND) + assertEquals(10012, FavoriteRelayListEvent.KIND) } /** @@ -49,8 +49,8 @@ class RelayFeedsListEventTest { fun removingOneRelayKeepsTheOtherPublicEntriesPublic() = runTest { val fromAnotherClient = - signer.sign<RelayFeedsListEvent>( - RelayFeedsListEvent.build( + signer.sign<FavoriteRelayListEvent>( + FavoriteRelayListEvent.build( publicRelays = listOf(damus, nosLol), signer = signer, createdAt = 1740669816, @@ -60,7 +60,7 @@ class RelayFeedsListEventTest { assertEquals(listOf(damus, nosLol), fromAnotherClient.publicRelays()) val afterRemoval = - RelayFeedsListEvent.updateRelayList( + FavoriteRelayListEvent.updateRelayList( earlierVersion = fromAnotherClient, relays = listOf(damus), signer = signer, @@ -79,8 +79,8 @@ class RelayFeedsListEventTest { fun privateRelaysStayPrivate() = runTest { val mixed = - signer.sign<RelayFeedsListEvent>( - RelayFeedsListEvent.build( + signer.sign<FavoriteRelayListEvent>( + FavoriteRelayListEvent.build( publicRelays = listOf(damus), privateRelays = listOf(nosLol, primal), signer = signer, @@ -89,7 +89,7 @@ class RelayFeedsListEventTest { ) val afterRemoval = - RelayFeedsListEvent.updateRelayList( + FavoriteRelayListEvent.updateRelayList( earlierVersion = mixed, relays = listOf(damus, nosLol), signer = signer, @@ -104,8 +104,8 @@ class RelayFeedsListEventTest { fun newRelaysFollowTheConventionOfTheList() = runTest { val publicOnly = - signer.sign<RelayFeedsListEvent>( - RelayFeedsListEvent.build( + signer.sign<FavoriteRelayListEvent>( + FavoriteRelayListEvent.build( publicRelays = listOf(damus), signer = signer, createdAt = 1740669816, @@ -113,7 +113,7 @@ class RelayFeedsListEventTest { ) val afterAdd = - RelayFeedsListEvent.updateRelayList( + FavoriteRelayListEvent.updateRelayList( earlierVersion = publicOnly, relays = listOf(damus, nosLol), signer = signer, @@ -127,8 +127,8 @@ class RelayFeedsListEventTest { ) val privateOnly = - signer.sign<RelayFeedsListEvent>( - RelayFeedsListEvent.build( + signer.sign<FavoriteRelayListEvent>( + FavoriteRelayListEvent.build( privateRelays = listOf(damus), signer = signer, createdAt = 1740669816, @@ -136,7 +136,7 @@ class RelayFeedsListEventTest { ) val afterPrivateAdd = - RelayFeedsListEvent.updateRelayList( + FavoriteRelayListEvent.updateRelayList( earlierVersion = privateOnly, relays = listOf(damus, nosLol), signer = signer, @@ -151,8 +151,8 @@ class RelayFeedsListEventTest { fun keepsUnrelatedTags() = runTest { val withTitle = - signer.sign<RelayFeedsListEvent>( - RelayFeedsListEvent.build( + signer.sign<FavoriteRelayListEvent>( + FavoriteRelayListEvent.build( publicRelays = listOf(damus, nosLol), signer = signer, createdAt = 1740669816, @@ -162,7 +162,7 @@ class RelayFeedsListEventTest { ) val afterRemoval = - RelayFeedsListEvent.updateRelayList( + FavoriteRelayListEvent.updateRelayList( earlierVersion = withTitle, relays = listOf(nosLol), signer = signer, diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/RelayListPublicEntriesTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/RelayListPublicEntriesTest.kt index a4eb27a872..5111ca4c15 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/RelayListPublicEntriesTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/RelayListPublicEntriesTest.kt @@ -62,8 +62,8 @@ class RelayListPublicEntriesTest { @Test fun relayFeedsListKeepsTheSurvivorPublic() = runTest { - val before = publicListFromAnotherClient<RelayFeedsListEvent>(RelayFeedsListEvent.KIND) - val after = RelayFeedsListEvent.updateRelayList(before, listOf(damus), signer, 1740669817) + val before = publicListFromAnotherClient<FavoriteRelayListEvent>(FavoriteRelayListEvent.KIND) + val after = FavoriteRelayListEvent.updateRelayList(before, listOf(damus), signer, 1740669817) assertEquals(listOf(damus), after.tags.relays()) } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip52Calendar/CalendarHintProviderTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip52Calendar/CalendarHintProviderTest.kt index dba5cf91cd..42fc62755e 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip52Calendar/CalendarHintProviderTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip52Calendar/CalendarHintProviderTest.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent -import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent +import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent import com.vitorpamplona.quartz.nip52Calendar.rsvp.CalendarRSVPEvent import kotlin.test.Test import kotlin.test.assertEquals @@ -66,7 +66,7 @@ class CalendarHintProviderTest { ) private fun calendar(vararg tags: Array<String>) = - CalendarEvent( + CalendarCollectionEvent( id = "33".repeat(32), pubKey = host, createdAt = 1700000000, diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiCollaborationTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiCollaborationTest.kt index 6a81dd3b56..1d015a2d68 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiCollaborationTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiCollaborationTest.kt @@ -36,7 +36,7 @@ class WikiCollaborationTest { private fun mergeRequest(vararg tags: Array<String>) = WikiMergeRequestEvent("id", "pk", 0L, arrayOf(*tags), "why", "sig") - private fun coordinate() = "${WikiNoteEvent.KIND}:$destination:hot-ice-creams" + private fun coordinate() = "${WikiArticleEvent.KIND}:$destination:hot-ice-creams" // ---- kind 818 -------------------------------------------------------------------------- @@ -115,7 +115,7 @@ class WikiCollaborationTest { fun buildsWithTheSpecMarker() { val template = WikiMergeRequestEvent.build( - targetArticle = Address(WikiNoteEvent.KIND, destination, "hot-ice-creams"), + targetArticle = Address(WikiArticleEvent.KIND, destination, "hot-ice-creams"), destinationAuthor = destination, mergeSourceId = fork, explanation = "I added a section", @@ -180,7 +180,7 @@ class WikiCollaborationTest { 0L, arrayOf( arrayOf("d", "shell-structure"), - arrayOf("a", "${WikiNoteEvent.KIND}:$destination:thin-shell-structure"), + arrayOf("a", "${WikiArticleEvent.KIND}:$destination:thin-shell-structure"), ), "", "sig", @@ -232,11 +232,11 @@ class WikiCollaborationTest { @Test fun buildsARedirectWithANormalizedSlug() { val template = - WikiRedirectEvent.build("Shell Structure", Address(WikiNoteEvent.KIND, destination, "thin-shell-structure")) + WikiRedirectEvent.build("Shell Structure", Address(WikiArticleEvent.KIND, destination, "thin-shell-structure")) val tags = template.tags.associate { it[0] to it[1] } assertEquals("shell-structure", tags["d"]) - assertEquals("${WikiNoteEvent.KIND}:$destination:thin-shell-structure", tags["a"]) + assertEquals("${WikiArticleEvent.KIND}:$destination:thin-shell-structure", tags["a"]) assertEquals("", template.content) } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestAnonTagTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestAnonTagTest.kt index e22a2030d5..9592029d3c 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestAnonTagTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestAnonTagTest.kt @@ -57,8 +57,8 @@ class LnZapRequestAnonTagTest { sig = "b".repeat(128), ) - private suspend fun request(zapType: LnZapEvent.ZapType) = - LnZapRequestEvent.create( + private suspend fun request(zapType: ZapReceiptEvent.ZapType) = + ZapRequestEvent.create( zappedEvent = zappedEvent, relays = relays, signer = signer, @@ -71,7 +71,7 @@ class LnZapRequestAnonTagTest { @Test fun `public zap request has no anon tag and is signed by the sender`() = runTest { - val zapRequest = request(LnZapEvent.ZapType.PUBLIC) + val zapRequest = request(ZapReceiptEvent.ZapType.PUBLIC) assertFalse(zapRequest.hasAnonTag()) assertFalse(zapRequest.isPrivateZap()) @@ -81,7 +81,7 @@ class LnZapRequestAnonTagTest { @Test fun `anonymous zap request has anon tag but is not private`() = runTest { - val zapRequest = request(LnZapEvent.ZapType.ANONYMOUS) + val zapRequest = request(ZapReceiptEvent.ZapType.ANONYMOUS) assertTrue(zapRequest.hasAnonTag()) assertFalse(zapRequest.isPrivateZap()) @@ -98,12 +98,12 @@ class LnZapRequestAnonTagTest { fun `anonymous profile zap request keeps the message public`() = runTest { val zapRequest = - LnZapRequestEvent.create( + ZapRequestEvent.create( userHex = receiverPubKey, relays = relays, signer = signer, message = "great work", - zapType = LnZapEvent.ZapType.ANONYMOUS, + zapType = ZapReceiptEvent.ZapType.ANONYMOUS, ) assertTrue(zapRequest.hasAnonTag()) @@ -115,7 +115,7 @@ class LnZapRequestAnonTagTest { @Test fun `private zap request has anon tag and is private and hides the sender key`() = runTest { - val zapRequest = request(LnZapEvent.ZapType.PRIVATE) + val zapRequest = request(ZapReceiptEvent.ZapType.PRIVATE) assertTrue(zapRequest.hasAnonTag()) assertTrue(zapRequest.isPrivateZap()) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestGroupTagTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestGroupTagTest.kt index 2553b9e31d..07451237b4 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestGroupTagTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestGroupTagTest.kt @@ -63,13 +63,13 @@ class LnZapRequestGroupTagTest { ) private suspend fun zapRequestFor(zapped: Event) = - LnZapRequestEvent.create( + ZapRequestEvent.create( zappedEvent = zapped, relays = relays, signer = nostrSigner, pollOption = null, message = "", - zapType = LnZapEvent.ZapType.PUBLIC, + zapType = ZapReceiptEvent.ZapType.PUBLIC, toUserPubHex = null, ) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestKindTagTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestKindTagTest.kt index 0ba8202418..1ba8c3c279 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestKindTagTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestKindTagTest.kt @@ -52,13 +52,13 @@ class LnZapRequestKindTagTest { val relays = setOf(NormalizedRelayUrl("wss://relay.example.com/")) val zapRequest = - LnZapRequestEvent.create( + ZapRequestEvent.create( zappedEvent = publicMsg, relays = relays, signer = nostrSigner, pollOption = null, message = "", - zapType = LnZapEvent.ZapType.PUBLIC, + zapType = ZapReceiptEvent.ZapType.PUBLIC, toUserPubHex = null, ) @@ -84,13 +84,13 @@ class LnZapRequestKindTagTest { val relays = setOf(NormalizedRelayUrl("wss://relay.example.com/")) val zapRequest = - LnZapRequestEvent.create( + ZapRequestEvent.create( zappedEvent = kind1Event, relays = relays, signer = nostrSigner, pollOption = null, message = "", - zapType = LnZapEvent.ZapType.PUBLIC, + zapType = ZapReceiptEvent.ZapType.PUBLIC, toUserPubHex = null, ) @@ -105,12 +105,12 @@ class LnZapRequestKindTagTest { val relays = setOf(NormalizedRelayUrl("wss://relay.example.com/")) val zapRequest = - LnZapRequestEvent.create( + ZapRequestEvent.create( userHex = receiverPubKey, relays = relays, signer = nostrSigner, message = "", - zapType = LnZapEvent.ZapType.PUBLIC, + zapType = ZapReceiptEvent.ZapType.PUBLIC, ) val kTag = zapRequest.tags.firstOrNull { it[0] == "k" } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/LnZapReceiptValidatorTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/LnZapReceiptValidatorTest.kt index a3ce50b8d2..ef3344b0b0 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/LnZapReceiptValidatorTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/LnZapReceiptValidatorTest.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip57Zaps.validate -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs @@ -62,13 +62,13 @@ class LnZapReceiptValidatorTest { signerPubkey: String = providerPubkey, bolt11: String? = invoice100kSats, description: String? = zapRequestJson(), - ): LnZapEvent { + ): ZapReceiptEvent { val tags = mutableListOf<Array<String>>() tags.add(arrayOf("p", recipientPubkey)) if (bolt11 != null) tags.add(arrayOf("bolt11", bolt11)) if (description != null) tags.add(arrayOf("description", description)) - return LnZapEvent( + return ZapReceiptEvent( id = "f".repeat(64), pubKey = signerPubkey, createdAt = 1700000001, diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/NutzapInfoEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/NutzapInfoEventTest.kt index 4b9c4dd50e..06eb717482 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/NutzapInfoEventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/NutzapInfoEventTest.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip61Nutzaps import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent import com.vitorpamplona.quartz.nip61Nutzaps.info.tags.NutzapMintTag import com.vitorpamplona.quartz.utils.nsecToKeyPair @@ -75,7 +75,7 @@ class NutzapInfoEventTest { fun deletionTargetsTheReplaceableAddress() = runTest { val empty = signer.sign(NutzapInfoEvent.buildEmpty()) - val deletion = signer.sign(DeletionEvent.build(listOf(empty))) + val deletion = signer.sign(DeletionRequestEvent.build(listOf(empty))) // NIP-09 deletion of a replaceable event needs the `a` address // coordinate so compliant relays drop all versions, plus the `e` diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/NutzapRedemptionTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/NutzapRedemptionTest.kt new file mode 100644 index 0000000000..28500b137b --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/NutzapRedemptionTest.kt @@ -0,0 +1,61 @@ +/* + * 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.quartz.nip61Nutzaps + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip60Cashu.history.CashuSpendingHistoryEvent +import com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenEvent +import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent +import com.vitorpamplona.quartz.nip61Nutzaps.redemption.buildNutzapRedemption +import com.vitorpamplona.quartz.utils.EventFactory +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +/** + * NIP-61 reuses the NIP-60 wallet kinds: redeemed tokens are kind 7375 wallet token + * events and redemptions are kind 7376 spending history events. Each kind has a + * single class, so what EventFactory builds must expose the NIP-61 fields. + */ +class NutzapRedemptionTest { + private val sender = "1".repeat(64) + private val nutzap = NutzapEvent("a".repeat(64), sender, 1, emptyArray(), "", "b".repeat(128)) + + @Test + fun redemptionIsParsedAsSpendingHistory() { + val template = CashuSpendingHistoryEvent.buildNutzapRedemption(EventHintBundle(nutzap), "encrypted") + assertEquals(CashuSpendingHistoryEvent.KIND, template.kind) + + val event = EventFactory.create<Event>("c".repeat(64), "2".repeat(64), 2, template.kind, template.tags, template.content, "d".repeat(128)) + val history = assertIs<CashuSpendingHistoryEvent>(event) + + assertEquals(listOf(nutzap.id), history.redeemedNutzaps().map { it.eventId }) + assertEquals(listOf(nutzap.id), history.redeemedReferences().map { it.eventId }) + assertEquals(listOf(sender), history.linkedPubKeys()) + } + + @Test + fun walletTokenKindHasOneClass() { + val event = EventFactory.create<Event>("c".repeat(64), "2".repeat(64), 2, CashuTokenEvent.KIND, emptyArray(), "", "d".repeat(128)) + assertIs<CashuTokenEvent>(event) + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip71Video/AddressableVideoEventAddressTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip71Video/AddressableVideoEventAddressTest.kt index e31e88f745..283756ee98 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip71Video/AddressableVideoEventAddressTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip71Video/AddressableVideoEventAddressTest.kt @@ -53,7 +53,7 @@ class AddressableVideoEventAddressTest { @Test fun verticalVideoAddressUsesDTag() { val event = - VideoVerticalEvent( + AddressableShortVideoEvent( id = "bfe2f2244fefc7cebc7b2eae825495f99dabb4649ee3f90ab1fa33bcd1e9bb9f", pubKey = pubkey, createdAt = 1780894816, @@ -62,13 +62,13 @@ class AddressableVideoEventAddressTest { sig = "", ) - assertAddressUsesDTag(VideoVerticalEvent.KIND, event) + assertAddressUsesDTag(AddressableShortVideoEvent.KIND, event) } @Test fun horizontalVideoAddressUsesDTag() { val event = - VideoHorizontalEvent( + AddressableNormalVideoEvent( id = "bfe2f2244fefc7cebc7b2eae825495f99dabb4649ee3f90ab1fa33bcd1e9bb9f", pubKey = pubkey, createdAt = 1780894816, @@ -77,13 +77,13 @@ class AddressableVideoEventAddressTest { sig = "", ) - assertAddressUsesDTag(VideoHorizontalEvent.KIND, event) + assertAddressUsesDTag(AddressableNormalVideoEvent.KIND, event) } @Test fun videoWithoutDTagFallsBackToEmptyAddress() { val event = - VideoVerticalEvent( + AddressableShortVideoEvent( id = "bfe2f2244fefc7cebc7b2eae825495f99dabb4649ee3f90ab1fa33bcd1e9bb9f", pubKey = pubkey, createdAt = 1780894816, @@ -93,7 +93,7 @@ class AddressableVideoEventAddressTest { ) assertEquals("", event.dTag()) - assertEquals(Address(VideoVerticalEvent.KIND, pubkey, ""), event.address()) - assertEquals("${VideoVerticalEvent.KIND}:$pubkey:", event.addressTag()) + assertEquals(Address(AddressableShortVideoEvent.KIND, pubkey, ""), event.address()) + assertEquals("${AddressableShortVideoEvent.KIND}:$pubkey:", event.addressTag()) } } diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/marmot/MarmotMipBehaviorTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/marmot/MarmotMipBehaviorTest.kt index 711852c67e..32d09a3046 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/marmot/MarmotMipBehaviorTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/marmot/MarmotMipBehaviorTest.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import kotlinx.coroutines.runBlocking import kotlin.test.Test @@ -350,8 +350,8 @@ class MarmotMipBehaviorTest { // Bob unwraps: kind:1059 → kind:13 (Seal) → kind:444 (Rumor). val sealed = delivery.giftWrapEvent.unwrapThrowing(bobSigner) - assertEquals(SealedRumorEvent.KIND, sealed.kind, "Middle layer MUST be NIP-59 Seal kind:13") - assertIs<SealedRumorEvent>(sealed) + assertEquals(SealEvent.KIND, sealed.kind, "Middle layer MUST be NIP-59 Seal kind:13") + assertIs<SealEvent>(sealed) val rumor = sealed.unsealThrowing(bobSigner) assertEquals(WelcomeEvent.KIND, rumor.kind, "Innermost rumor MUST be kind:444") diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapperTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapperTest.kt index fb949ca47d..76022f4303 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapperTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapperTest.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.nip01Core.jackson import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor import kotlin.test.Test import kotlin.test.assertContentEquals @@ -46,7 +46,7 @@ class JacksonMapperTest { ) val followCard = - FollowListEvent( + StarterPackEvent( id = "eca31634fce7c9068b56fa8db9f387da70bdcceb3986a77ca1a9844f3128eb5f", pubKey = "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da", createdAt = 1761736286, diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt index 40e7aa5884..a3ee938277 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt @@ -39,7 +39,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse -import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor import kotlin.test.Test import kotlin.test.assertContentEquals @@ -65,7 +65,7 @@ class KotlinSerializationMapperTest { ) val followCard = - FollowListEvent( + StarterPackEvent( id = "eca31634fce7c9068b56fa8db9f387da70bdcceb3986a77ca1a9844f3128eb5f", pubKey = "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da", createdAt = 1761736286, @@ -194,7 +194,7 @@ class KotlinSerializationMapperTest { fun deserializeEventWithSpecialCharactersInContent() { val content = "Hello \"world\" \n\ttab\\backslash" val event = - FollowListEvent( + StarterPackEvent( id = "abc", pubKey = "def", createdAt = 1000, @@ -769,7 +769,7 @@ class KotlinSerializationMapperTest { @Test fun emptyContentEvent() { val event = - FollowListEvent( + StarterPackEvent( id = "a".repeat(64), pubKey = "b".repeat(64), createdAt = 0, @@ -796,7 +796,7 @@ class KotlinSerializationMapperTest { fun eventWithUnicodeContent() { val content = "Hello \uD83D\uDE00 world \u00E9\u00E8\u00EA" val event = - FollowListEvent( + StarterPackEvent( id = "a".repeat(64), pubKey = "b".repeat(64), createdAt = 1000, diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/RelayGroupFilterServingRelayTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/RelayGroupFilterServingRelayTest.kt index 1bf3edf89c..bd1395c3df 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/RelayGroupFilterServingRelayTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip29RelayGroups/RelayGroupFilterServingRelayTest.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupAdminsEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMembersEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMetadataEvent import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupPinnedEvent -import com.vitorpamplona.quartz.nip29RelayGroups.metadata.SupportedRolesEvent +import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupRolesEvent import com.vitorpamplona.quartz.nip29RelayGroups.tags.GroupIdTag import com.vitorpamplona.quartz.nip7DThreads.ThreadEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent @@ -72,7 +72,7 @@ class RelayGroupFilterServingRelayTest : RelayClientTest() { GroupMetadataEvent.KIND -> GroupMetadataEvent(id, relayKey, 100L, tags, "", sig) GroupAdminsEvent.KIND -> GroupAdminsEvent(id, relayKey, 100L, tags, "", sig) GroupMembersEvent.KIND -> GroupMembersEvent(id, relayKey, 100L, tags, "", sig) - SupportedRolesEvent.KIND -> SupportedRolesEvent(id, relayKey, 100L, tags, "", sig) + GroupRolesEvent.KIND -> GroupRolesEvent(id, relayKey, 100L, tags, "", sig) GroupPinnedEvent.KIND -> GroupPinnedEvent(id, relayKey, 100L, tags, "", sig) else -> error("unexpected state kind $kind") } @@ -105,7 +105,7 @@ class RelayGroupFilterServingRelayTest : RelayClientTest() { state(1, GroupMetadataEvent.KIND), state(2, GroupAdminsEvent.KIND), state(3, GroupMembersEvent.KIND), - state(4, SupportedRolesEvent.KIND), + state(4, GroupRolesEvent.KIND), state(5, GroupPinnedEvent.KIND), ), ) @@ -119,7 +119,7 @@ class RelayGroupFilterServingRelayTest : RelayClientTest() { GroupMetadataEvent.KIND, GroupAdminsEvent.KIND, GroupMembersEvent.KIND, - SupportedRolesEvent.KIND, + GroupRolesEvent.KIND, GroupPinnedEvent.KIND, ), tags = mapOf("d" to listOf("g1")), @@ -268,7 +268,7 @@ class RelayGroupFilterServingRelayTest : RelayClientTest() { GroupMetadataEvent.KIND, GroupAdminsEvent.KIND, GroupMembersEvent.KIND, - SupportedRolesEvent.KIND, + GroupRolesEvent.KIND, ), limit = 500, ), diff --git a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsEventStore.kt b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsEventStore.kt index 38cc465830..62616dd7ef 100644 --- a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsEventStore.kt +++ b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsEventStore.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.quartz.nip01Core.store.IEventStore import com.vitorpamplona.quartz.nip01Core.store.sqlite.DefaultIndexingStrategy import com.vitorpamplona.quartz.nip01Core.store.sqlite.IndexingStrategy import com.vitorpamplona.quartz.nip01Core.store.sqlite.TagNameValueHasher -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.nip50Search.strippingSearchExtensions @@ -136,7 +136,7 @@ open class FsEventStore( if (slot != null && existingSlot?.id != event.id) { slots.install(slot, canonical, event, existingSlot) } - if (event is DeletionEvent) processDeletion(event, canonical) + if (event is DeletionRequestEvent) processDeletion(event, canonical) if (event is RequestToVanishEvent) processVanish(event, canonical) return } @@ -158,7 +158,7 @@ open class FsEventStore( if (slot != null) { slots.install(slot, canonical, event, existingSlot) } - if (event is DeletionEvent) processDeletion(event, canonical) + if (event is DeletionRequestEvent) processDeletion(event, canonical) if (event is RequestToVanishEvent) processVanish(event, canonical) } catch (t: Throwable) { Files.deleteIfExists(tmp) @@ -223,7 +223,7 @@ open class FsEventStore( * future re-inserts are blocked. */ private fun processDeletion( - deletion: DeletionEvent, + deletion: DeletionRequestEvent, deletionCanonical: java.nio.file.Path, ) { val ownerHashOfDeletion = hasher.hash(deletion.pubKey) diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/buzz/interop/BuzzRelayLiveInteropTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/buzz/interop/BuzzRelayLiveInteropTest.kt index 429936584d..fd513e3b49 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/buzz/interop/BuzzRelayLiveInteropTest.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/buzz/interop/BuzzRelayLiveInteropTest.kt @@ -29,8 +29,8 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip29RelayGroups.moderation.CreateGroupEvent -import com.vitorpamplona.quartz.nip29RelayGroups.request.JoinRequestEvent -import com.vitorpamplona.quartz.nip29RelayGroups.request.LeaveRequestEvent +import com.vitorpamplona.quartz.nip29RelayGroups.request.GroupJoinRequestEvent +import com.vitorpamplona.quartz.nip29RelayGroups.request.GroupLeaveRequestEvent import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import kotlinx.coroutines.runBlocking import okhttp3.OkHttpClient @@ -238,7 +238,7 @@ class BuzzRelayLiveInteropTest { val (ws2, frames2) = authedSocket(joiner) val join = joiner.sign( - JoinRequestEvent.build(channelId), + GroupJoinRequestEvent.build(channelId), ) ws2.send("""["EVENT",${join.toJson()}]""") val joinOk = frames2.nextOf("OK") @@ -250,7 +250,7 @@ class BuzzRelayLiveInteropTest { assertTrue("true" in frames2.nextOf("OK"), "post after join should be accepted") // LEAVE: kind 9022, the NIP-29 leave request. - val leave = joiner.sign(LeaveRequestEvent.build(channelId)) + val leave = joiner.sign(GroupLeaveRequestEvent.build(channelId)) ws2.send("""["EVENT",${leave.toJson()}]""") assertTrue("true" in frames2.nextOf("OK"), "leave (9022) should be accepted") diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsAuthorsMissingOutboxTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsAuthorsMissingOutboxTest.kt index 09238d03b2..1d57a7d042 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsAuthorsMissingOutboxTest.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsAuthorsMissingOutboxTest.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.store.fs import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent @@ -95,7 +95,7 @@ class FsAuthorsMissingOutboxTest { store.insert(relayList) assertEquals(emptySet(), store.authorsMissingOutbox().toSet()) - store.insert(signer.sign(DeletionEvent.build(listOf(relayList)))) + store.insert(signer.sign(DeletionRequestEvent.build(listOf(relayList)))) assertEquals(setOf(signer.pubKey), store.authorsMissingOutbox().toSet()) } } diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsDeletionTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsDeletionTest.kt index 88cc2d05b5..cbb23808b2 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsDeletionTest.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsDeletionTest.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.quartz.nip01Core.store.fs import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import kotlinx.coroutines.runBlocking import java.nio.file.Files import java.nio.file.Path @@ -67,9 +67,9 @@ class FsDeletionTest { body: String, ts: Long, signer: NostrSignerSync = this.signer, - ) = signer.sign<LongTextNoteEvent>( + ) = signer.sign<LongFormContentEvent>( createdAt = ts, - kind = LongTextNoteEvent.KIND, + kind = LongFormContentEvent.KIND, tags = arrayOf(arrayOf("d", slug)), content = body, ) @@ -92,12 +92,12 @@ class FsDeletionTest { store.insert(n1) store.insert(n2) - val del = signer.sign<DeletionEvent>(DeletionEvent.build(listOf(n1), createdAt = 30)) + val del = signer.sign<DeletionRequestEvent>(DeletionRequestEvent.build(listOf(n1), createdAt = 30)) store.insert(del) assertEquals(emptyList(), store.query<TextNoteEvent>(Filter(ids = listOf(n1.id))).map { it.id }) assertEquals(listOf(n2.id), store.query<TextNoteEvent>(Filter(ids = listOf(n2.id))).map { it.id }) - assertEquals(listOf(del.id), store.query<DeletionEvent>(Filter(ids = listOf(del.id))).map { it.id }) + assertEquals(listOf(del.id), store.query<DeletionRequestEvent>(Filter(ids = listOf(del.id))).map { it.id }) } @Test @@ -106,7 +106,7 @@ class FsDeletionTest { val n1 = note("one", 10) store.insert(n1) - val del = signer.sign<DeletionEvent>(DeletionEvent.build(listOf(n1), createdAt = 30)) + val del = signer.sign<DeletionRequestEvent>(DeletionRequestEvent.build(listOf(n1), createdAt = 30)) store.insert(del) store.insert(n1) // should be blocked @@ -121,7 +121,7 @@ class FsDeletionTest { store.insert(theirs) // Our signer attempts to delete it. - val del = signer.sign<DeletionEvent>(DeletionEvent.build(listOf(theirs), createdAt = 30)) + val del = signer.sign<DeletionRequestEvent>(DeletionRequestEvent.build(listOf(theirs), createdAt = 30)) store.insert(del) // Cascade did NOT run — not our author. @@ -150,14 +150,14 @@ class FsDeletionTest { store.insert(v1) store.insert(v2) - val del = signer.sign<DeletionEvent>(DeletionEvent.build(listOf(v2), createdAt = 30)) + val del = signer.sign<DeletionRequestEvent>(DeletionRequestEvent.build(listOf(v2), createdAt = 30)) store.insert(del) // Slot cleared, canonical removed, indexes gone. val dHash = FsLayout.sha256Hex("intro") - val slot = root.resolve("addressable/${LongTextNoteEvent.KIND}/${signer.pubKey}/$dHash.json") + val slot = root.resolve("addressable/${LongFormContentEvent.KIND}/${signer.pubKey}/$dHash.json") assertFalse(slot.exists(), "addressable slot should be cleared") - assertEquals(emptyList(), store.query<LongTextNoteEvent>(Filter(authors = listOf(signer.pubKey), kinds = listOf(LongTextNoteEvent.KIND))).map { it.id }) + assertEquals(emptyList(), store.query<LongFormContentEvent>(Filter(authors = listOf(signer.pubKey), kinds = listOf(LongFormContentEvent.KIND))).map { it.id }) } @Test @@ -166,14 +166,14 @@ class FsDeletionTest { val v1 = article("intro", "draft 1", 10) store.insert(v1) - val del = signer.sign<DeletionEvent>(DeletionEvent.build(listOf(v1), createdAt = 20)) + val del = signer.sign<DeletionRequestEvent>(DeletionRequestEvent.build(listOf(v1), createdAt = 20)) store.insert(del) // A newer addressable at the same address should still be accepted. val v3 = article("intro", "draft 3", 30) store.insert(v3) - val got = store.query<LongTextNoteEvent>(Filter(authors = listOf(signer.pubKey), kinds = listOf(LongTextNoteEvent.KIND))) + val got = store.query<LongFormContentEvent>(Filter(authors = listOf(signer.pubKey), kinds = listOf(LongFormContentEvent.KIND))) assertEquals(listOf(v3.id), got.map { it.id }) } @@ -183,13 +183,13 @@ class FsDeletionTest { val v1 = article("intro", "draft 1", 10) store.insert(v1) - val del = signer.sign<DeletionEvent>(DeletionEvent.build(listOf(v1), createdAt = 20)) + val del = signer.sign<DeletionRequestEvent>(DeletionRequestEvent.build(listOf(v1), createdAt = 20)) store.insert(del) // Attempting to re-insert an event authored earlier than the deletion should fail. val older = article("intro", "even-older", 5) store.insert(older) - assertEquals(emptyList(), store.query<LongTextNoteEvent>(Filter(ids = listOf(older.id))).map { it.id }) + assertEquals(emptyList(), store.query<LongFormContentEvent>(Filter(ids = listOf(older.id))).map { it.id }) } @Test @@ -198,12 +198,12 @@ class FsDeletionTest { val v = article("intro", "v", 10) store.insert(v) - val del = signer.sign<DeletionEvent>(DeletionEvent.build(listOf(v), createdAt = 15)) + val del = signer.sign<DeletionRequestEvent>(DeletionRequestEvent.build(listOf(v), createdAt = 15)) store.insert(del) val equal = article("intro", "equal", 15) store.insert(equal) - assertEquals(emptyList(), store.query<LongTextNoteEvent>(Filter(ids = listOf(equal.id))).map { it.id }) + assertEquals(emptyList(), store.query<LongFormContentEvent>(Filter(ids = listOf(equal.id))).map { it.id }) } // ------------------------------------------------------------------ @@ -216,18 +216,18 @@ class FsDeletionTest { val v = article("intro", "v", 10) store.insert(v) - val del1 = signer.sign<DeletionEvent>(DeletionEvent.build(listOf(v), createdAt = 20)) + val del1 = signer.sign<DeletionRequestEvent>(DeletionRequestEvent.build(listOf(v), createdAt = 20)) store.insert(del1) val del2Target = article("intro", "v2", 30) // inserted only to give del2 a target store.insert(del2Target) - val del2 = signer.sign<DeletionEvent>(DeletionEvent.build(listOf(del2Target), createdAt = 40)) + val del2 = signer.sign<DeletionRequestEvent>(DeletionRequestEvent.build(listOf(del2Target), createdAt = 40)) store.insert(del2) // Cutoff should now be 40, so an event at createdAt=35 is blocked. val mid = article("intro", "mid", 35) store.insert(mid) - assertEquals(emptyList(), store.query<LongTextNoteEvent>(Filter(ids = listOf(mid.id))).map { it.id }) + assertEquals(emptyList(), store.query<LongFormContentEvent>(Filter(ids = listOf(mid.id))).map { it.id }) } @Test @@ -238,23 +238,23 @@ class FsDeletionTest { store.insert(v1) store.insert(v2) - val strongDel = signer.sign<DeletionEvent>(DeletionEvent.build(listOf(v2), createdAt = 100)) + val strongDel = signer.sign<DeletionRequestEvent>(DeletionRequestEvent.build(listOf(v2), createdAt = 100)) store.insert(strongDel) // Now insert a weaker (earlier) deletion for the same address. val weakTarget = article("slug", "target-for-weak", 30) store.insert(weakTarget) // this passes? No: cutoff=100, target@30 is blocked. Actually we want to - // construct a DeletionEvent that targets the slug address directly. The simplest way: + // construct a DeletionRequestEvent that targets the slug address directly. The simplest way: val weakDel = - signer.sign<DeletionEvent>( - DeletionEvent.buildAddressOnly(listOf(v1), createdAt = 50), + signer.sign<DeletionRequestEvent>( + DeletionRequestEvent.buildAddressOnly(listOf(v1), createdAt = 50), ) store.insert(weakDel) // Cutoff should still be 100 — an event at 60 must still be blocked. val blocked = article("slug", "should-be-blocked", 60) store.insert(blocked) - assertEquals(emptyList(), store.query<LongTextNoteEvent>(Filter(ids = listOf(blocked.id))).map { it.id }) + assertEquals(emptyList(), store.query<LongFormContentEvent>(Filter(ids = listOf(blocked.id))).map { it.id }) } // ------------------------------------------------------------------ @@ -266,10 +266,10 @@ class FsDeletionTest { runBlocking { val n = note("x", 10) store.insert(n) - val del = signer.sign<DeletionEvent>(DeletionEvent.build(listOf(n), createdAt = 20)) + val del = signer.sign<DeletionRequestEvent>(DeletionRequestEvent.build(listOf(n), createdAt = 20)) store.insert(del) - val byKind = store.query<DeletionEvent>(Filter(kinds = listOf(DeletionEvent.KIND))) + val byKind = store.query<DeletionRequestEvent>(Filter(kinds = listOf(DeletionRequestEvent.KIND))) assertEquals(listOf(del.id), byKind.map { it.id }) } @@ -289,10 +289,10 @@ class FsDeletionTest { // `event_tags.pubkey_hash = NEW.pubkey_owner_hash` guard. val v1 = otherArticle("shared", "v1", 10) store.insert(v1) - assertEquals(listOf(v1.id), store.query<LongTextNoteEvent>(Filter(ids = listOf(v1.id))).map { it.id }) + assertEquals(listOf(v1.id), store.query<LongFormContentEvent>(Filter(ids = listOf(v1.id))).map { it.id }) val strangerDel = - signer.sign<DeletionEvent>(DeletionEvent.build(listOf(v1), createdAt = 20)) + signer.sign<DeletionRequestEvent>(DeletionRequestEvent.build(listOf(v1), createdAt = 20)) store.insert(strangerDel) // Bob can still publish a newer version at the same address. Since @@ -300,7 +300,7 @@ class FsDeletionTest { // exists to block. val v2 = otherArticle("shared", "v2", 30) store.insert(v2) - assertEquals(listOf(v2.id), store.query<LongTextNoteEvent>(Filter(ids = listOf(v2.id))).map { it.id }) + assertEquals(listOf(v2.id), store.query<LongFormContentEvent>(Filter(ids = listOf(v2.id))).map { it.id }) } @Test @@ -308,7 +308,7 @@ class FsDeletionTest { runBlocking { val n = note("x", 10) store.insert(n) - val del = signer.sign<DeletionEvent>(DeletionEvent.build(listOf(n), createdAt = 20)) + val del = signer.sign<DeletionRequestEvent>(DeletionRequestEvent.build(listOf(n), createdAt = 20)) store.insert(del) val tomb = root.resolve("tombstones/id/${n.id}.json") diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsParityTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsParityTest.kt index a9603f8996..0a79e052d5 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsParityTest.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsParityTest.kt @@ -25,9 +25,9 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventStore -import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import kotlinx.coroutines.runBlocking import java.nio.file.Files import java.nio.file.Path @@ -121,10 +121,10 @@ class FsParityTest { slug: String, body: String, ts: Long, - ): LongTextNoteEvent = + ): LongFormContentEvent = signer.sign( createdAt = ts, - kind = LongTextNoteEvent.KIND, + kind = LongFormContentEvent.KIND, tags = arrayOf(arrayOf("d", slug)), content = body, ) @@ -234,7 +234,7 @@ class FsParityTest { insertBoth(v2) insertBoth(v3) - assertParity(Filter(authors = listOf(signer.pubKey), kinds = listOf(LongTextNoteEvent.KIND))) + assertParity(Filter(authors = listOf(signer.pubKey), kinds = listOf(LongFormContentEvent.KIND))) } @Test @@ -284,7 +284,7 @@ class FsParityTest { assertParity( Filter( authors = listOf(signer.pubKey), - kinds = listOf(LongTextNoteEvent.KIND), + kinds = listOf(LongFormContentEvent.KIND), tags = mapOf("d" to listOf("tie")), ), ) @@ -303,12 +303,12 @@ class FsParityTest { insertBoth(a) insertBoth(b) - val del = signer.sign<DeletionEvent>(DeletionEvent.build(listOf(a), createdAt = 30)) + val del = signer.sign<DeletionRequestEvent>(DeletionRequestEvent.build(listOf(a), createdAt = 30)) insertBoth(del) assertParity(Filter(ids = listOf(a.id))) assertParity(Filter(ids = listOf(b.id))) - assertParity(Filter(kinds = listOf(DeletionEvent.KIND))) + assertParity(Filter(kinds = listOf(DeletionRequestEvent.KIND))) // Re-insert blocked. insertBoth(a) @@ -321,16 +321,16 @@ class FsParityTest { val v = article("intro", "v1", 10) insertBoth(v) - val del = signer.sign<DeletionEvent>(DeletionEvent.build(listOf(v), createdAt = 20)) + val del = signer.sign<DeletionRequestEvent>(DeletionRequestEvent.build(listOf(v), createdAt = 20)) insertBoth(del) - assertParity(Filter(authors = listOf(signer.pubKey), kinds = listOf(LongTextNoteEvent.KIND))) + assertParity(Filter(authors = listOf(signer.pubKey), kinds = listOf(LongFormContentEvent.KIND))) // Older event at this address must be blocked, newer must pass. insertBoth(article("intro", "older", 5)) insertBoth(article("intro", "newer", 100)) - assertParity(Filter(authors = listOf(signer.pubKey), kinds = listOf(LongTextNoteEvent.KIND))) + assertParity(Filter(authors = listOf(signer.pubKey), kinds = listOf(LongFormContentEvent.KIND))) } // ------------------------------------------------------------------ @@ -429,7 +429,7 @@ class FsParityTest { val artB = article("b", "B v1", 30) val artBv2 = article("b", "B v2", 50) // Deletion of n1 - val del = signer.sign<DeletionEvent>(DeletionEvent.build(listOf(n1), createdAt = 40)) + val del = signer.sign<DeletionRequestEvent>(DeletionRequestEvent.build(listOf(n1), createdAt = 40)) listOf(n1, n2, meta1, meta2, artA, artB, artBv2, del).forEach { insertBoth(it) } @@ -437,8 +437,8 @@ class FsParityTest { assertParity(Filter(ids = listOf(n1.id)), "n1 deleted") assertParity(Filter(ids = listOf(n2.id)), "n2 alive") assertParity(Filter(authors = listOf(signer.pubKey), kinds = listOf(0)), "metadata winner") - assertParity(Filter(authors = listOf(signer.pubKey), kinds = listOf(LongTextNoteEvent.KIND)), "articles set") - assertParity(Filter(authors = listOf(signer.pubKey), kinds = listOf(DeletionEvent.KIND)), "deletion present") + assertParity(Filter(authors = listOf(signer.pubKey), kinds = listOf(LongFormContentEvent.KIND)), "articles set") + assertParity(Filter(authors = listOf(signer.pubKey), kinds = listOf(DeletionRequestEvent.KIND)), "deletion present") } // ------------------------------------------------------------------ diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsSearchTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsSearchTest.kt index ccd9400ae9..ddfdadacb0 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsSearchTest.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsSearchTest.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import kotlinx.coroutines.runBlocking import java.nio.file.Files import java.nio.file.Path @@ -166,7 +166,7 @@ class FsSearchTest { val n = note("uniqnote bitcoin", ts = 100) val long = signer.sign( - LongTextNoteEvent.build( + LongFormContentEvent.build( "uniqlong body", title = "title", dTag = "d1", diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsSlotsTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsSlotsTest.kt index 26ac6f6463..4ebda49500 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsSlotsTest.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/fs/FsSlotsTest.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import kotlinx.coroutines.runBlocking import java.nio.file.Files import java.nio.file.Path @@ -224,10 +224,10 @@ class FsSlotsTest { } val got = - store.query<LongTextNoteEvent>( + store.query<LongFormContentEvent>( Filter( authors = listOf(signer.pubKey), - kinds = listOf(LongTextNoteEvent.KIND), + kinds = listOf(LongFormContentEvent.KIND), tags = mapOf("d" to listOf("intro")), ), ) @@ -255,10 +255,10 @@ class FsSlotsTest { slug: String, body: String, createdAt: Long, - ): LongTextNoteEvent = + ): LongFormContentEvent = signer.sign( createdAt = createdAt, - kind = LongTextNoteEvent.KIND, + kind = LongFormContentEvent.KIND, tags = arrayOf(arrayOf("d", slug)), content = body, ) @@ -271,7 +271,7 @@ class FsSlotsTest { store.insert(v1) store.insert(v2) - val got = store.query<LongTextNoteEvent>(Filter(authors = listOf(signer.pubKey), kinds = listOf(LongTextNoteEvent.KIND))) + val got = store.query<LongFormContentEvent>(Filter(authors = listOf(signer.pubKey), kinds = listOf(LongFormContentEvent.KIND))) assertEquals(listOf(v2.id), got.map { it.id }) assertFalse(store.hasCanonical(v1.id), "older draft canonical should be removed") } @@ -284,7 +284,7 @@ class FsSlotsTest { store.insert(intro) store.insert(about) - val got = store.query<LongTextNoteEvent>(Filter(authors = listOf(signer.pubKey), kinds = listOf(LongTextNoteEvent.KIND))) + val got = store.query<LongFormContentEvent>(Filter(authors = listOf(signer.pubKey), kinds = listOf(LongFormContentEvent.KIND))) assertEquals(setOf(intro.id, about.id), got.map { it.id }.toSet()) } @@ -296,7 +296,7 @@ class FsSlotsTest { store.insert(newer) store.insert(older) - val got = store.query<LongTextNoteEvent>(Filter(authors = listOf(signer.pubKey))) + val got = store.query<LongFormContentEvent>(Filter(authors = listOf(signer.pubKey))) assertEquals(listOf(newer.id), got.map { it.id }) } @@ -307,7 +307,7 @@ class FsSlotsTest { store.insert(v) val dHash = FsLayout.sha256Hex("intro") - val slot = root.resolve("addressable/${LongTextNoteEvent.KIND}/${signer.pubKey}/$dHash.json") + val slot = root.resolve("addressable/${LongFormContentEvent.KIND}/${signer.pubKey}/$dHash.json") assertTrue(slot.exists()) val parsed = Event.fromJson(slot.readText()) assertEquals(v.id, parsed.id) @@ -320,7 +320,7 @@ class FsSlotsTest { store.insert(v) val dHash = FsLayout.sha256Hex("") - val slot = root.resolve("addressable/${LongTextNoteEvent.KIND}/${signer.pubKey}/$dHash.json") + val slot = root.resolve("addressable/${LongFormContentEvent.KIND}/${signer.pubKey}/$dHash.json") assertTrue(slot.exists()) } @@ -330,7 +330,7 @@ class FsSlotsTest { val v = article("intro", "hello", 10) store.insert(v) val dHash = FsLayout.sha256Hex("intro") - val slot = root.resolve("addressable/${LongTextNoteEvent.KIND}/${signer.pubKey}/$dHash.json") + val slot = root.resolve("addressable/${LongFormContentEvent.KIND}/${signer.pubKey}/$dHash.json") assertTrue(slot.exists()) store.delete(v.id) From 5e029cd9c05784c950d66999ce709542bd92698d Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 20:40:23 +0000 Subject: [PATCH 12/60] fix(quartz): conform the tagging d to the revised Tapestry rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Tapestry draft fixed this defect on its own `feat/tags` branch, dated "Superseded 2026-09-10", before it was reported. Same diagnosis that `1770ef27` reached — the old rule used only the coordinate's author segment, which is constant across everything that author writes — but a different wire format, so this branch has to follow rather than diverge. `event-taggings.md` § "The assertion d-tag (normative)" now reads, for an `a` target: d = event-tag-<slug>-<author8>-<d16>-<hash8>-<asserter8> `hash8` is the first 8 lowercase hex of SHA-256 over the UTF-8 bytes of the full coordinate `<kind>:<author>:<d>`, and is the only segment that carries uniqueness; `author8` and `d16` are readable decoration. `d16` is the first 16 UTF-16 code units of the target's own `d`, verbatim, which is what String.take counts. `1770ef27` collapsed the whole thing to just the hash, so it was correct but non-conformant. The `e` branch is untouched: an event id already covers the whole event, and the draft leaves it as the first 8 hex of the id. Nothing parses the `d` back into these fields, and the draft now forbids it outright, since `d16` is user-influenced text that can itself contain hyphens. The target is read from the `a`/`e` tag. Two cases the draft spells out are pinned alongside the three collision cases: an empty `d` leaves the double hyphen `<author8>--<hash8>`, and a `d` longer than 16 units is truncated in the decoration while `hash8` still separates. All hashes are SHA-256 prefixes computed outside this codebase. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXvKXakvup4inNFfAhhr4L --- .../taggings/EventTagging.kt | 46 +++++++++++++++---- .../decentralizedLists/AuditRegressionTest.kt | 37 +++++++++++---- .../TapestryExtensionsTest.kt | 7 +-- 3 files changed, 70 insertions(+), 20 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/EventTagging.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/EventTagging.kt index 782ad80571..8a45a5ef82 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/EventTagging.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/EventTagging.kt @@ -42,9 +42,10 @@ import com.vitorpamplona.quartz.utils.sha256.sha256 @Immutable sealed interface TaggingTarget { /** - * The 8 hex characters that stand for this target in the assertion `d`. Two distinct targets - * must not produce the same one: kind 39999 is addressable, so taggings that share a `d` do - * not sit side by side — the later one replaces the earlier. + * What stands for this target in the assertion `d`: `<id8>` for a plain event, + * `<author8>-<d16>-<hash8>` for an addressable one. Two distinct targets must never produce + * the same one — kind 39999 is addressable, so taggings sharing a `d` do not sit side by + * side: the later one replaces the earlier. */ val prefix: String @@ -52,17 +53,39 @@ sealed interface TaggingTarget { data class ByAddress( val address: Address, ) : TaggingTarget { - // Hashed over the WHOLE coordinate, because no single segment identifies the target: the - // author repeats across everything they write, and the kind across everything of a type. - // A plain event can use its own id (below) since that already covers the whole event. - override val prefix get() = sha256(address.toValue().encodeToByteArray()).toHexKey().take(8) + /** + * `<author8>-<d16>-<hash8>`. Only `hash8` carries uniqueness — it is taken over the WHOLE + * coordinate, because no single segment identifies the target: the author repeats across + * everything they write, and the kind across everything of a type. The other two are + * decoration the draft asks for so a `d` stays readable. + * + * An empty `dTag` yields an empty `d16`, i.e. `<author8>--<hash8>`, which the draft spells + * out. A plain event uses its own id (below): that already covers the whole event. + */ + override val prefix get() = "${address.pubKeyHex.take(AUTHOR_CHARS)}-${address.dTag.take(D_TAG_CHARS)}-${hash8()}" + + // Lowercase hex of the UTF-8 coordinate exactly as the `a` tag carries it: no trimming, + // no normalization, no reordering. Hex.encode already emits lower case. + private fun hash8() = sha256(address.toValue().encodeToByteArray()).toHexKey().take(HASH_CHARS) } @Immutable data class ByEventId( val eventId: HexKey, ) : TaggingTarget { - override val prefix get() = eventId.take(8) + override val prefix get() = eventId.take(EVENT_ID_CHARS) + } + + companion object { + const val EVENT_ID_CHARS = 8 + const val AUTHOR_CHARS = 8 + const val HASH_CHARS = 8 + + /** + * UTF-16 code units, which is what the draft pins ("as JavaScript `String.prototype.slice` + * counts them") and what [String.take] counts. + */ + const val D_TAG_CHARS = 16 } } @@ -113,7 +136,8 @@ object TaggingHeader { * [TaggingHeader]: * * ``` - * ["d", "event-tag-<tagSlug>-<target8>-<asserter8>"] + * ["d", "event-tag-<tagSlug>-<target8>-<asserter8>"] // <target8> = <id8>, or + * // <author8>-<d16>-<hash8> for an `a` * ["a", <target coordinate>] or ["e", <target id>] * ["z", <nostr-event-tag concept>] * ["z", <tagging header coordinate>] @@ -121,6 +145,10 @@ object TaggingHeader { * ``` * * An `a`/`e` reference without such a `z` is not a tagging and must not be read as one. + * + * The `d` is a replaceability key, never a source of truth: the draft forbids parsing it back into + * its fields, because `d16` is user-influenced text that may itself contain hyphens. [parse] reads + * the target from the `a`/`e` tag, which is authoritative. */ @Immutable data class EventTagging( diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/AuditRegressionTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/AuditRegressionTest.kt index e1f2f2c56a..fbf7384959 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/AuditRegressionTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/AuditRegressionTest.kt @@ -143,27 +143,48 @@ class AuditRegressionTest { } // ...while the `d` stays deterministic, which is what lets a re-tag replace its own assertion - // instead of piling up a second one. Pinned against SHA-256 prefixes of the coordinates - // computed outside this codebase, so the test cross-checks the derivation rather than - // restating it: a change of hash input would silently orphan every assertion already signed. + // instead of piling up a second one. `event-taggings.md` § "The assertion d-tag (normative)": + // `<author8>-<d16>-<hash8>`, only hash8 unique. The hashes are SHA-256 prefixes of the full + // coordinate computed outside this codebase, so the test cross-checks the derivation rather + // than restating it: a change of hash input would silently orphan every assertion signed. @Test - fun theAddressablePrefixIsTheHashOfTheWholeCoordinate() { + fun theAddressablePrefixIsAuthorThenDTagThenTheHashOfTheWholeCoordinate() { assertEquals( - "event-tag-awesome-tag-6a5e1c40-aaaaaaaa", + "event-tag-awesome-tag-bbbbbbbb-good-tag-6a5e1c40-aaaaaaaa", EventTagging.dTag("awesome-tag", TaggingTarget.ByAddress(Address(39999, bob, "good-tag")), alice), ) assertEquals( - "event-tag-awesome-tag-e89b797c-aaaaaaaa", + "event-tag-awesome-tag-bbbbbbbb-other-tag-e89b797c-aaaaaaaa", EventTagging.dTag("awesome-tag", TaggingTarget.ByAddress(Address(39999, bob, "other-tag")), alice), ) + // same author8 AND same d16 as the first; only the kind differs, so only hash8 separates them assertEquals( - "event-tag-awesome-tag-96856fd8-aaaaaaaa", + "event-tag-awesome-tag-bbbbbbbb-good-tag-96856fd8-aaaaaaaa", EventTagging.dTag("awesome-tag", TaggingTarget.ByAddress(Address(30023, bob, "good-tag")), alice), ) } + // The draft spells this case out: an empty `d` yields an empty `d16`, hence the double hyphen. + @Test + fun anEmptyDTagSegmentLeavesADoubleHyphen() { + assertEquals( + "event-tag-awesome-tag-bbbbbbbb--76600e1a-aaaaaaaa", + EventTagging.dTag("awesome-tag", TaggingTarget.ByAddress(Address(39999, bob, "")), alice), + ) + } + + // d16 is the first 16 UTF-16 code units, verbatim and truncated — decoration, not identity. + @Test + fun aLongDTagIsTruncatedToSixteenCharactersInTheDecoration() { + val d = "a-very-long-d-tag-that-exceeds-sixteen" + assertEquals( + "event-tag-awesome-tag-bbbbbbbb-a-very-long-d-ta-58899d62-aaaaaaaa", + EventTagging.dTag("awesome-tag", TaggingTarget.ByAddress(Address(39999, bob, d)), alice), + ) + } + // A plain event is still named by its own id: it already covers the whole event, and hashing - // it would only cost a round of SHA-256 per assertion built. + // it would only cost a round of SHA-256 per assertion built. The `e` branch is unchanged. @Test fun plainEventTargetsStillUseTheirOwnId() { assertEquals( diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/TapestryExtensionsTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/TapestryExtensionsTest.kt index c212e8deb2..f244c1da1d 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/TapestryExtensionsTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/TapestryExtensionsTest.kt @@ -247,9 +247,10 @@ class TapestryExtensionsTest { assertEquals( listOf( - // 4d7a80b1 is sha256("39999:${"c".repeat(64)}:good-tag").take(8) — the whole - // coordinate, so two of the assistant's tags cannot share this `d`. - listOf("d", "event-tag-awesome-tag-4d7a80b1-aaaaaaaa"), + // <author8>-<d16>-<hash8>: cccccccc is the assistant, good-tag the target's own + // `d`, and 4d7a80b1 = sha256("39999:${"c".repeat(64)}:good-tag").take(8), the one + // segment that makes two of the assistant's tags land on different addresses. + listOf("d", "event-tag-awesome-tag-cccccccc-good-tag-4d7a80b1-aaaaaaaa"), listOf("z", "39998:$ta:nostr-event-tag"), listOf("z", "39999:$bob:tagging:awesome-tag-tagging"), listOf("a", "39999:$assistant:good-tag"), From f72388a55937c573cab998931f10eb90b37253f2 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 20:56:07 +0000 Subject: [PATCH 13/60] refactor: rename packages, helpers and app code after the NIP event renames Follow-up to the event class renames: the names around those classes now match too. Quartz (published API, every old name kept as a @Deprecated alias/forwarder): - nip51Lists packages moved: followList -> starterPack, hashtagList -> interestList, peopleList -> followSet, labeledBookmarkList -> bookmarkSet. The old packages keep a Deprecated.kt with typealiases and forwarding extension functions (ReplaceWith points at the new package). - LnZapPrivateEvent -> PrivateZapEvent, LnZapReceiptValidator -> ZapReceiptValidator, ChannelListDiff -> PublicChatListDiff, HashtagListDiff -> InterestListDiff. - Kind 30063 now indexes NIP-82 release notes for search (never NIP-51 content, which can hold encrypted private items). searchable-events docs updated. App code (amethyst, commons, desktopApp, cli; no aliases needed): - model packages nip51Lists.{hashtagLists,peopleList,labeledBookmarkLists,relayFeeds} -> {interestLists,followSets,bookmarkSets,favoriteRelays}. - State/cache/UI names derived from the old event names: HashtagListState -> InterestListState, PeopleListsState -> FollowSetsState, FollowListsState -> StarterPacksState, LabeledBookmarkList -> BookmarkSet, RelayFeedListState -> FavoriteRelayListState, ContactCardsState -> UserAssertionsState, NIP90* view models/renderers -> Dvm*, LnZap* handlers -> Zap*, SealedRumor* -> Seal*, and Account.peopleLists/followLists/hashtagList -> followSets/starterPacks/interestList. - String literals were left untouched, so preference keys and @SerialName values stored on disk are unchanged. - The generic PeopleList UI model (shared by follow sets and starter packs) and the EmojiPackSelection route (which shows a kind 30030 pack) keep their names. Docs: plans, brainstorms, changelogs, skills and READMEs use the new names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015VvJ8XpeYqBe8bYT3bJ7UD --- .claude/skills/account-state/SKILL.md | 4 +- .../references/account-state-flow.md | 10 +-- .../skills/nip85-trusted-assertions/SKILL.md | 8 +- .claude/skills/searchable-events/SKILL.md | 7 +- .../references/searchable-kinds.md | 18 ++-- ...2026-07-01-auth-permission-architecture.md | 4 +- .../plans/2026-07-07-nip29-armada-study.md | 12 +-- .../2026-07-13-chat-scroll-performance.md | 2 +- .../2026-07-15-bitchat-geohash-interop.md | 2 +- .../2026-07-23-push-notification-redesign.md | 6 +- amethyst/plans/2026-07-24-nwc-bolt12-pay.md | 2 +- ...2026-07-31-shortpost-notify-list-picker.md | 6 +- .../2026-09-10-dvm-heartbeat-liveness-plan.md | 4 +- .../2026-09-10-dvm-heartbeat-liveness.md | 2 +- .../plans/2026-09-17-nip-cc-geocaching.md | 2 +- .../plans/archive/2026-05-14-onchain-zaps.md | 6 +- .../2026-06-22-napplet-nsite-security.md | 2 +- .../amethyst/LocalPreferences.kt | 32 +++---- .../vitorpamplona/amethyst/model/Account.kt | 82 ++++++++--------- .../amethyst/model/AccountSettings.kt | 40 ++++----- .../amethyst/model/EventBroadcaster.kt | 2 +- .../BookmarkSetsState.kt} | 84 ++++++++--------- .../FavoriteRelayListState.kt} | 50 +++++------ .../FollowSetsState.kt} | 18 ++-- .../StarterPacksState.kt} | 36 ++++---- .../InterestListState.kt} | 34 +++---- .../preferences/AccountPreferenceStores.kt | 4 +- .../serverList/MergedFollowListsState.kt | 30 +++---- .../notifications/NotificationDispatcher.kt | 6 +- .../notifications/PushWrapDecryptor.kt | 2 +- .../FilterAccountInfoAndListsFromKey.kt | 6 +- .../reqCommand/user/UserObservers.kt | 38 ++++---- .../resourceusage/MeteringNostrSigner.kt | 4 +- .../amethyst/ui/dal/FilterByListParams.kt | 2 +- .../amethyst/ui/feeds/PrefetchFeedMedia.kt | 4 +- .../ui/navigation/drawer/DrawerContent.kt | 4 +- .../ui/navigation/routes/RouteMaker.kt | 2 +- .../amethyst/ui/note/MultiSetCompose.kt | 4 +- .../amethyst/ui/note/NoteCompose.kt | 40 ++++----- .../amethyst/ui/note/NoteQuickActionMenu.kt | 4 +- .../ui/note/UpdateReactionTypeDialog.kt | 2 +- .../amethyst/ui/note/UserProfilePicture.kt | 4 +- .../ui/note/creators/notify/AudienceSheet.kt | 6 +- .../amethyst/ui/note/elements/DropDownMenu.kt | 4 +- .../ui/note/types/ActivityCardPreviews.kt | 10 +-- .../amethyst/ui/note/types/Emoji.kt | 2 +- .../amethyst/ui/note/types/FileHeader.kt | 8 +- .../types/NIP90ContentDiscoveryResponse.kt | 2 +- .../amethyst/ui/note/types/NIP90Status.kt | 2 +- .../amethyst/ui/note/types/PeopleList.kt | 2 +- .../amethyst/ui/note/types/ProfileCard.kt | 4 +- .../amethyst/ui/note/types/RelayList.kt | 6 +- .../types/{FollowList.kt => StarterPack.kt} | 4 +- .../amethyst/ui/note/types/Wiki.kt | 4 +- .../amethyst/ui/note/types/ZapEvent.kt | 6 +- .../ui/note/types/lists/BookmarkLists.kt | 4 +- .../amethyst/ui/note/types/lists/TagLists.kt | 4 +- .../ui/screen/AccountSessionManager.kt | 8 +- .../amethyst/ui/screen/TopNavFilterState.kt | 20 ++--- .../ui/screen/loggedIn/AccountViewModel.kt | 6 +- .../loggedIn/DecryptAndIndexProcessor.kt | 22 ++--- .../loggedIn/backups/BackupConflictCards.kt | 14 +-- .../backups/BackupConflictListViews.kt | 12 +-- .../backups/BackupConflictPresentation.kt | 12 +-- .../backups/BackupConflictReviewScreen.kt | 8 +- .../display/BookmarkGroupScreen.kt | 2 +- .../display/BookmarkGroupViewModel.kt | 14 +-- .../list/ListOfBookmarkGroupsScreen.kt | 18 ++-- .../BookmarkGroupMetadataViewModel.kt | 10 +-- .../ArticleBookmarkListManagementScreen.kt | 10 +-- .../PostBookmarkListManagementScreen.kt | 10 +-- .../relayGroup/RelayGroupDiscoveryScreen.kt | 4 +- .../relayGroup/RelayGroupMembersScreen.kt | 2 +- .../RelayGroupsDiscoverySubAssembler.kt | 4 +- .../loggedIn/discover/ChannelCardCompose.kt | 2 +- .../loggedIn/discover/DiscoverScreen.kt | 2 +- .../DiscoverFollowSetsFeedFilter.kt | 2 +- .../discover/nip51FollowSets/FollowSetCard.kt | 2 +- .../dvms/DvmContentDiscoveryScreen.kt | 12 +-- ...kt => DvmContentDiscoveryFeedViewModel.kt} | 6 +- ...t => DvmContentDiscoveryResponseFilter.kt} | 2 +- .../emojipacks/list/ListOfEmojiPacksScreen.kt | 2 +- .../EmojiPackSelectionScreen.kt | 2 +- .../membershipManagement/MyEmojiListScreen.kt | 2 +- .../followPacks/feed/FollowPackFeedScreen.kt | 2 +- .../FollowPackFeedConversationsFeedFilter.kt | 2 +- .../dal/FollowPackFeedNewThreadFeedFilter.kt | 2 +- .../feed/dal/FollowPackMembersFeedFilter.kt | 2 +- .../FollowPackFeedFilterSubAssembler.kt | 2 +- .../followPacks/list/FollowPacksFeedLoaded.kt | 2 +- .../followPacks/list/FollowPacksTopBar.kt | 2 +- .../display/lists/PeopleListViewModel.kt | 12 +-- .../display/packs/FollowPackViewModel.kt | 12 +-- .../lists/list/FollowPackViewModel.kt | 8 +- .../lists/list/PeopleListViewModel.kt | 8 +- .../metadata/FollowPackMetadataViewModel.kt | 8 +- .../metadata/PeopleListMetadataViewModel.kt | 8 +- .../FollowListAndPackAndUserView.kt | 12 +-- .../profile/followers/FollowersTabHeader.kt | 4 +- .../profile/header/UserNicknameCard.kt | 4 +- .../profile/header/UserProfileDropDownMenu.kt | 4 +- .../loggedIn/relays/AllRelayListScreen.kt | 6 +- .../screen/loggedIn/relays/KindDisplayName.kt | 12 +-- ...Model.kt => FavoriteRelayListViewModel.kt} | 6 +- .../relays/feeds/RelayFeedsListView.kt | 6 +- .../loggedIn/threadview/ThreadFeedView.kt | 20 ++--- ...dCompose.kt => FileMetadataCardCompose.kt} | 16 ++-- .../ui/screen/loggedIn/video/VideoScreen.kt | 2 +- .../resourceusage/ResourceUsageLedgerTest.kt | 4 +- cli/plans/archive/2026-04-23-nip17-dm.md | 6 +- commons/plans/2026-05-04-custom-feeds-plan.md | 6 +- .../2026-08-30-commons-migration-sweep.md | 4 +- .../commons/account/AccountBootstrapEvents.kt | 12 +-- .../backups/ReplaceableBackupConflict.kt | 2 +- .../commons/model/cache/EventCache.kt | 28 +++--- .../nip28PublicChats/PublicChatListState.kt | 28 +++--- .../nip29RelayGroups/RelayGroupChannel.kt | 14 +-- .../model/nip30CustomEmojis/EmojiPackState.kt | 18 ++-- .../blockPeopleList/BlockPeopleListState.kt | 6 +- .../BookmarkSet.kt} | 4 +- .../FavoriteRelayListDecryptionCache.kt} | 4 +- .../FollowSetDecryptionCache.kt} | 6 +- .../{peopleList => followSets}/PeopleList.kt | 2 +- .../InterestListDecryptionCache.kt} | 8 +- ...che.kt => UserAssertionDecryptionCache.kt} | 2 +- ...ctCardsState.kt => UserAssertionsState.kt} | 4 +- .../model/topNavFeeds/FeedDecryptionCaches.kt | 8 +- .../topNavFeeds/noteBased/NoteFeedFlow.kt | 6 +- .../notifications/NotificationKinds.kt | 2 +- .../FilterBasicAccountInfoFromKeys.kt | 2 +- .../FilterBookmarksAndReportsFromKey.kt | 2 +- .../metadata/FilterFollowsAndMutesFromKey.kt | 4 +- .../assemblers/ContactCardFilters.kt | 10 +-- .../FilterFollowSetsByAllCommunities.kt | 2 +- .../FilterFollowSetsByAuthors.kt | 2 +- .../FilterFollowSetsByCommunity.kt | 2 +- .../FilterFollowSetsByGeohash.kt | 2 +- .../FilterFollowSetsByHashtag.kt | 2 +- .../nip51FollowSets/FilterFollowSetsGlobal.kt | 2 +- .../profile/FilterUserProfileLists.kt | 6 +- .../user/watchers/UserCardsSubAssembler.kt | 6 +- .../amethyst/commons/search/KindRegistry.kt | 2 +- .../commons/search/RenderableKinds.kt | 4 +- .../model/privateChats/ChatPreviewTest.kt | 14 +-- .../notifications/NotificationKindsTest.kt | 10 +-- .../commons/napplet/NappletBrokerTest.kt | 4 +- .../commons/onchain/OnchainZapSenderTest.kt | 4 +- .../commons/viewmodels/NestViewModelTest.kt | 4 +- .../nip51Lists/ui/BookmarkGroupItem.kt | 4 +- .../ui/ListOfBookmarkGroupsFeedView.kt | 14 +-- .../commons/nip51Lists/ui/PeopleListItem.kt | 2 +- .../ui/EditNicknameDialog.kt | 12 +-- ...sktop-wallet-zapping-test-coverage-plan.md | 16 ++-- ...-23-feat-desktop-moderation-safety-plan.md | 10 +-- ...07-27-feat-desktop-profile-actions-plan.md | 34 +++---- ...-07-27-feat-desktop-profile-parity-plan.md | 24 ++--- ...26-07-27-feat-desktop-profile-tabs-plan.md | 2 +- ...ta-loading-upstream-reconciliation-plan.md | 2 +- .../2026-05-09-embedded-local-relay-plan.md | 2 +- .../2026-05-11-wallet-zapping-brainstorm.md | 2 +- .../desktop/cache/DesktopLocalCache.kt | 14 +-- .../desktop/followpacks/BulkFollowAction.kt | 2 +- .../desktop/followpacks/FollowPackEditor.kt | 2 +- .../desktop/followpacks/FollowPacksState.kt | 2 +- .../desktop/followpacks/ui/DiscoverScreen.kt | 2 +- .../followpacks/ui/FollowAllConfirmDialog.kt | 2 +- .../ui/FollowPackBrowseAllScreen.kt | 2 +- .../followpacks/ui/FollowPackComponents.kt | 2 +- .../followpacks/ui/FollowPackDetailScreen.kt | 2 +- .../followpacks/ui/RenderFollowPackCard.kt | 2 +- .../desktop/model/DesktopHiddenUsersState.kt | 8 +- .../amethyst/desktop/ui/UserProfileScreen.kt | 2 +- .../desktop/ui/note/DesktopRichText.kt | 2 +- docs/TODO.md | 2 +- .../2026-03-16-desktop-media-brainstorm.md | 2 +- ...26-03-19-dm-encryption-badge-brainstorm.md | 2 +- docs/changelog/v0.31.04.md | 2 +- docs/changelog/v0.76.01.md | 2 +- docs/changelog/v1.03.00.md | 2 +- docs/changelog/v1.12.00.md | 2 +- docs/changelog/v1.12.02.md | 2 +- ...-06-10-feat-desktop-dm-reliability-plan.md | 4 +- ...sktop-cache-navigation-persistence-plan.md | 6 +- ...clean-cache-single-source-of-truth-plan.md | 10 +-- ...3-24-feat-article-highlights-notes-plan.md | 6 +- ...026-04-21-feat-relay-config-parity-plan.md | 8 +- ...-21-feat-relay-subscription-wiring-plan.md | 2 +- ...-22-feat-relay-subscription-wiring-plan.md | 4 +- ...feat-desktop-multi-account-support-plan.md | 2 +- ...fix-bunker-timeouts-and-decryption-plan.md | 4 +- ...22-feat-desktop-note-action-bar-ux-plan.md | 2 +- ...2-fix-desktop-feed-review-findings-plan.md | 6 +- .../2026-06-12-giftwrap-deletion-requests.md | 2 +- .../plans/2026-07-11-concord-event-classes.md | 4 +- quartz/plans/archive/2026-06-09-clink.md | 4 +- ...entTest.kt => NwcRequestNip44EventTest.kt} | 2 +- .../nip55AndroidSigner/api/SignerResult.kt | 4 +- .../api/background/queries/DecryptZapQuery.kt | 4 +- .../intents/responses/DecryptZapResponse.kt | 6 +- .../client/NostrSignerExternal.kt | 4 +- .../vitorpamplona/quartz/kinds/KindNames.kt | 12 +-- .../quartz/marmot/MarmotInboundProcessor.kt | 2 +- .../marmot/mip02Welcome/WelcomeGiftWrap.kt | 4 +- .../quartz/nip01Core/signers/NostrSigner.kt | 4 +- .../nip01Core/signers/NostrSignerInternal.kt | 4 +- .../nip01Core/signers/NostrSignerSync.kt | 4 +- .../quartz/nip13Pow/signer/PoWNostrSigner.kt | 4 +- ...annelListDiff.kt => PublicChatListDiff.kt} | 8 +- .../list/PublicChatListEvent.kt | 6 +- .../signer/NostrSignerRemote.kt | 4 +- .../nip50Search/SearchFieldExtractor.kt | 8 +- .../BookmarkSetEvent.kt | 8 +- .../TagArrayBuilderExt.kt | 2 +- .../nip51Lists/followList/Deprecated.kt | 90 +++++++++++++++++++ .../FollowSetEvent.kt | 8 +- .../TagArrayBuilderExt.kt | 2 +- .../{peopleList => followSet}/TagArrayExt.kt | 2 +- .../nip51Lists/hashtagList/Deprecated.kt | 61 +++++++++++++ .../InterestListDiff.kt} | 4 +- .../InterestListEvent.kt | 14 +-- .../TagArrayBuilderExt.kt | 2 +- .../TagArrayExt.kt | 2 +- .../labeledBookmarkList/Deprecated.kt | 58 ++++++++++++ .../mediaStarterPack/MediaStarterPackEvent.kt | 6 +- .../nip51Lists/peopleList/Deprecated.kt | 83 +++++++++++++++++ .../ReleaseArtifactSetEvent.kt | 10 ++- .../StarterPackEvent.kt | 8 +- .../TagArrayBuilderExt.kt | 2 +- .../TagArrayExt.kt | 2 +- .../nip57Zaps/IPrivateZapsDecryptionCache.kt | 4 +- .../quartz/nip57Zaps/PrivateZapCache.kt | 6 +- ...nZapPrivateEvent.kt => PrivateZapEvent.kt} | 12 ++- .../nip57Zaps/PrivateZapRequestBuilder.kt | 8 +- .../validate/LnurlEndpointResolver.kt | 2 +- ...iptValidator.kt => ZapReceiptValidator.kt} | 8 +- .../clientTag/NostrSignerWithClientTag.kt | 4 +- .../quartz/utils/EventFactory.kt | 12 +-- .../SoftwareApplicationEventTest.kt | 26 ++++++ ...ameTest.kt => UserAssertionPetNameTest.kt} | 2 +- .../store/sqlite/QueryAssemblerTest.kt | 2 +- .../BunkerRequestProcessorConcurrencyTest.kt | 4 +- .../server/BunkerRequestProcessorTest.kt | 6 +- .../server/NostrConnectSignerServiceTest.kt | 4 +- ...entTest.kt => NwcRequestNip44EventTest.kt} | 2 +- .../nip50Search/SearchFieldExtractorTest.kt | 6 +- .../relayLists/RelayListPublicEntriesTest.kt | 2 +- ...nonTagTest.kt => ZapRequestAnonTagTest.kt} | 2 +- ...upTagTest.kt => ZapRequestGroupTagTest.kt} | 2 +- ...indTagTest.kt => ZapRequestKindTagTest.kt} | 2 +- ...atorTest.kt => ZapReceiptValidatorTest.kt} | 66 +++++++------- .../nip01Core/jackson/JacksonMapperTest.kt | 2 +- .../KotlinSerializationMapperTest.kt | 2 +- 252 files changed, 1262 insertions(+), 945 deletions(-) rename amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/{labeledBookmarkLists/LabeledBookmarkListsState.kt => bookmarkSets/BookmarkSetsState.kt} (80%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/{relayFeeds/RelayFeedListState.kt => favoriteRelays/FavoriteRelayListState.kt} (65%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/{peopleList/PeopleListsState.kt => followSets/FollowSetsState.kt} (95%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/{peopleList/FollowListsState.kt => followSets/StarterPacksState.kt} (91%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/{hashtagLists/HashtagListState.kt => interestLists/InterestListState.kt} (80%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/{FollowList.kt => StarterPack.kt} (99%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/{NIP90ContentDiscoveryFeedViewModel.kt => DvmContentDiscoveryFeedViewModel.kt} (89%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/{NIP90ContentDiscoveryResponseFilter.kt => DvmContentDiscoveryResponseFilter.kt} (98%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/{RelayFeedsListViewModel.kt => FavoriteRelayListViewModel.kt} (90%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/{FileHeaderCardCompose.kt => FileMetadataCardCompose.kt} (93%) rename commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/{labeledBookmarkLists/LabeledBookmarkList.kt => bookmarkSets/BookmarkSet.kt} (94%) rename commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/{relayFeeds/RelayFeedsListDecryptionCache.kt => favoriteRelays/FavoriteRelayListDecryptionCache.kt} (92%) rename commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/{peopleList/PeopleListDecryptionCache.kt => followSets/FollowSetDecryptionCache.kt} (94%) rename commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/{peopleList => followSets}/PeopleList.kt (96%) rename commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/{hashtagLists/HashtagListDecryptionCache.kt => interestLists/InterestListDecryptionCache.kt} (86%) rename commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/{ContactCardDecryptionCache.kt => UserAssertionDecryptionCache.kt} (98%) rename commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/{ContactCardsState.kt => UserAssertionsState.kt} (98%) rename quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/{LnZapPaymentRequestNip44EventTest.kt => NwcRequestNip44EventTest.kt} (98%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/{ChannelListDiff.kt => PublicChatListDiff.kt} (84%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/{labeledBookmarkList => bookmarkSet}/BookmarkSetEvent.kt (97%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/{labeledBookmarkList => bookmarkSet}/TagArrayBuilderExt.kt (97%) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/Deprecated.kt rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/{peopleList => followSet}/FollowSetEvent.kt (98%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/{peopleList => followSet}/TagArrayBuilderExt.kt (97%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/{peopleList => followSet}/TagArrayExt.kt (96%) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/Deprecated.kt rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/{hashtagList/HashtagListDiff.kt => interestList/InterestListDiff.kt} (95%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/{hashtagList => interestList}/InterestListEvent.kt (95%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/{hashtagList => interestList}/TagArrayBuilderExt.kt (96%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/{hashtagList => interestList}/TagArrayExt.kt (96%) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/Deprecated.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/Deprecated.kt rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/{followList => starterPack}/StarterPackEvent.kt (95%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/{followList => starterPack}/TagArrayBuilderExt.kt (97%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/{followList => starterPack}/TagArrayExt.kt (96%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/{LnZapPrivateEvent.kt => PrivateZapEvent.kt} (87%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/{LnZapReceiptValidator.kt => ZapReceiptValidator.kt} (95%) rename quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip85TrustedAssertions/{ContactCardPetNameTest.kt => UserAssertionPetNameTest.kt} (99%) rename quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/{LnZapPaymentRequestNip44EventTest.kt => NwcRequestNip44EventTest.kt} (98%) rename quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/{LnZapRequestAnonTagTest.kt => ZapRequestAnonTagTest.kt} (99%) rename quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/{LnZapRequestGroupTagTest.kt => ZapRequestGroupTagTest.kt} (99%) rename quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/{LnZapRequestKindTagTest.kt => ZapRequestKindTagTest.kt} (99%) rename quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/{LnZapReceiptValidatorTest.kt => ZapReceiptValidatorTest.kt} (70%) diff --git a/.claude/skills/account-state/SKILL.md b/.claude/skills/account-state/SKILL.md index 528a2dd273..6abc975cb4 100644 --- a/.claude/skills/account-state/SKILL.md +++ b/.claude/skills/account-state/SKILL.md @@ -43,10 +43,10 @@ Relay frame ──► LocalCache.insertOrUpdateNote() ──► LocalCacheFlow e - `kind3FollowList = Kind3FollowListState(...)` ← NIP-02 ContactList (kind 3) - `nip65RelayList = Nip65RelayListState(...)` ← NIP-65 RelayList (kind 10002), plus siblings `dmRelayList`, `searchRelayList`, `blockedRelayList`, `trustedRelayList`, `proxyRelayList`, `broadcastRelayList`, `indexerRelayList`, … - `muteList = MuteListState(...)` ← NIP-51 MuteList (kind 10000) - - `bookmarkState = BookmarkListState(...)` ← NIP-51 Bookmarks (kind 10003), plus `labeledBookmarkLists`, `pinState`, `interestSets`, `peopleLists`, `followLists`, `hashtagList`, `geohashList`, `communityList`, `emoji`, `blossomServers`, … + - `bookmarkState = BookmarkListState(...)` ← NIP-51 Bookmarks (kind 10003), plus `bookmarkSets`, `pinState`, `interestSets`, `peopleLists`, `followLists`, `hashtagList`, `geohashList`, `communityList`, `emoji`, `blossomServers`, … - Derived/merged views: `hiddenUsers`, `allFollows`, `homeRelays`, `outboxRelays`, `dmRelays`, `notificationRelays`, `trustedRelays`, and the `live*FollowListsPerRelay` outbox loaders. - **The pattern:** each `XState` class pins its addressable note via `cache.getOrCreateAddressableNote(address)` (a long-term reference so GC/eviction can't drop it), exposes `val flow: StateFlow<…>` derived from the note's metadata flow (decrypted through a per-feature `DecryptionCache`, with backup fallback from `AccountSettings`, `stateIn(scope, Eagerly, …)`), and offers suspend mutation helpers (e.g. `MuteListState.hideUser(pubkey)`) that build the updated signed event. Consumers read `account.muteList.flow`, never a raw `MutableStateFlow` on `Account`. -- Encrypted lists pair the state object with a `DecryptionCache` sibling (`muteListDecryptionCache`, `peopleListDecryptionCache`, …) so NIP-44 decryption results are cached per event. +- Encrypted lists pair the state object with a `DecryptionCache` sibling (`muteListDecryptionCache`, `followSetDecryptionCache`, …) so NIP-44 decryption results are cached per event. - UI reads via `collectAsStateWithLifecycle` on Android and `collectAsState` on Desktop. - Sibling files per feature live alongside: `AccountSettings.kt`, `AccountSyncedSettings.kt`, plus per-NIP state classes under `model/nip02FollowLists/`, `model/nip51Lists/`, `model/nip65RelayList/`, etc. diff --git a/.claude/skills/account-state/references/account-state-flow.md b/.claude/skills/account-state/references/account-state-flow.md index d7e37c5524..73a56cca1b 100644 --- a/.claude/skills/account-state/references/account-state-flow.md +++ b/.claude/skills/account-state/references/account-state-flow.md @@ -16,8 +16,8 @@ been renamed, grep `Account.kt` for the class name.) | `userMetadata` | `UserMetadataState` | 0 | `amethyst/.../model/nip01UserMetadata/` | | `kind3FollowList` | `Kind3FollowListState` | 3 | `model/nip02FollowLists/` | | `muteList` (+ `muteListDecryptionCache`) | `MuteListState` | 10000 | `model/nip51Lists/muteList/` | -| `blockPeopleList`, `peopleLists` | `BlockPeopleListState`, `PeopleListsState` | NIP-51 people sets | `model/nip51Lists/peopleList/` | -| `followLists` | `FollowListsState` | NIP-51 follow sets | `model/nip51Lists/peopleList/` | +| `blockPeopleList`, `peopleLists` | `BlockPeopleListState`, `FollowSetsState` | NIP-51 people sets | `model/nip51Lists/followSets/` | +| `followLists` | `StarterPacksState` | NIP-51 follow sets | `model/nip51Lists/followSets/` | | `hiddenUsers` | `HiddenUsersState` — derived from `muteList.flow` + `blockPeopleList.flow` | — | `model/nip51Lists/` | | `allFollows` | `MergedFollowListsState` — merges kind3 + people/follow/hashtag/geohash/community lists | — | `model/serverList/` | @@ -31,7 +31,7 @@ been renamed, grep `Account.kt` for the class name.) | `blockedRelayList` | `BlockedRelayListState` | 10006 | `model/nip51Lists/blockedRelays/` | | `localRelayList` | `LocalRelayListState` | local | `model/localRelays/` | | `privateStorageRelayList` | `PrivateStorageRelayListState` | private storage | `model/edits/` | -| `keyPackageRelayList`, `trustedRelayList`, `proxyRelayList`, `broadcastRelayList`, `indexerRelayList`, `relayFeedsList` | per-feature `…RelayListState` classes, each with a `DecryptionCache` sibling | custom relay sets | `model/nip51Lists/…` | +| `keyPackageRelayList`, `trustedRelayList`, `proxyRelayList`, `broadcastRelayList`, `indexerRelayList`, `favoriteRelayList` | per-feature `…RelayListState` classes, each with a `DecryptionCache` sibling | custom relay sets | `model/nip51Lists/…` | Derived relay views (merge several of the above): `homeRelays` (`AccountHomeRelayState`), `outboxRelays`, `dmRelays`, `notificationRelays`, @@ -43,10 +43,10 @@ Derived relay views (merge several of the above): `homeRelays` | Account property | State class | Kind | Package | |------------------|-------------|------|---------| | `bookmarkState` (and legacy `oldBookmarkState`) | `BookmarkListState` | 10003 | `model/nip51Lists/` | -| `labeledBookmarkLists` | `LabeledBookmarkListsState` | NIP-51 bookmark sets | `model/nip51Lists/labeledBookmarkLists/` | +| `bookmarkSets` | `BookmarkSetsState` | NIP-51 bookmark sets | `model/nip51Lists/bookmarkSets/` | | `pinState` | `PinListState` | NIP-51 | `model/nip51Lists/` | | `interestSets` | `InterestSetsState` | NIP-51 interest sets | `model/nip51Lists/interestSets/` | -| `hashtagList` / `geohashList` | `HashtagListState` / `GeohashListState` | NIP-51 | `model/nip51Lists/hashtagLists/`, `…/geohashLists/` | +| `hashtagList` / `geohashList` | `InterestListState` / `GeohashListState` | NIP-51 | `model/nip51Lists/interestLists/`, `…/geohashLists/` | | `communityList` | `CommunityListState` | NIP-72 communities | `model/nip72Communities/` | | `favoriteAlgoFeedsList` | `FavoriteAlgoFeedsListState` | NIP-51 | `model/nip51Lists/` | | `emoji`, `ownedEmojiPacks` | `EmojiPackState`, `OwnedEmojiPacksState` | 10030 | `commons/.../commons/model/nip30CustomEmojis/` | diff --git a/.claude/skills/nip85-trusted-assertions/SKILL.md b/.claude/skills/nip85-trusted-assertions/SKILL.md index d75415e4df..935ea8e3d4 100644 --- a/.claude/skills/nip85-trusted-assertions/SKILL.md +++ b/.claude/skills/nip85-trusted-assertions/SKILL.md @@ -92,8 +92,8 @@ The same kind serves two roles, distinguished **by author**: 2. **The account's own contact cards (nicknames, NIP-81-style)** — signed by the account, one per target user. The petname, summary, and their NIP-30 emoji mappings **always live in the NIP-44 encrypted content, never in public tags** (`UserAssertionEvent.build`/ - `updatePetNameAndSummary` strip stray public copies; asserted by `ContactCardPetNameTest`). - `commons/.../ContactCardsState.kt` keys everything on `author == account` and ignores + `updatePetNameAndSummary` strip stray public copies; asserted by `UserAssertionPetNameTest`). + `commons/.../UserAssertionsState.kt` keys everything on `author == account` and ignores provider cards. ## Tag vocabulary and value semantics @@ -225,8 +225,8 @@ card's `a`-tag (`30382:<provider>:<target>`). - **Publisher**: `quartz/.../experimental/graperank/GrapeRankPublisher.kt` (canonical 30382 writer), `cli/.../graperank/` (`amy graperank register|unregister|providers|publish`). -- **Client model**: `commons/.../model/nip85TrustedAssertions/` (`ContactCardsState`, - `UserCardsCache`, `ContactCardDecryptionCache`, `TrustProviderListDecryptionCache`), +- **Client model**: `commons/.../model/nip85TrustedAssertions/` (`UserAssertionsState`, + `UserCardsCache`, `UserAssertionDecryptionCache`, `TrustProviderListDecryptionCache`), `amethyst/.../model/trustedAssertions/TrustProviderListState.kt`. - **Relay plumbing**: `commons/.../relayClient/assemblers/ContactCardFilters.kt`, `amethyst/.../reqCommand/user/watchers/UserCardsSubAssembler.kt`. diff --git a/.claude/skills/searchable-events/SKILL.md b/.claude/skills/searchable-events/SKILL.md index aad4744f68..f27492bf5f 100644 --- a/.claude/skills/searchable-events/SKILL.md +++ b/.claude/skills/searchable-events/SKILL.md @@ -45,10 +45,9 @@ Notables that surprise people: (`zapRequest?.content.orEmpty()`) — receipts are searchable by the zapper's comment. - **Kind 0 / 31990** index many profile fields space-joined (name, about, nip05, lud16, website, picture URL, …). -- **Kind 30063 is claimed twice** (`ReleaseArtifactSetEvent` in nip51Lists and the experimental - `SoftwareReleaseEvent`); `EventFactory` resolves 30063 to `ReleaseArtifactSetEvent`, so - `title()\ndescription()` is what actually gets indexed — `SoftwareReleaseEvent.indexableContent()` - is dead on the store path. +- **Kind 30063** (`ReleaseArtifactSetEvent`) serves both NIP-51 release artifact sets and NIP-82 + software releases: it indexes `title()\ndescription()`, plus the release notes in `content` only + for NIP-82 releases (NIP-51 `content` can be encrypted private items). - Poll kinds (1068, 6969) append each option label on its own line. ## MANDATORY maintenance when you touch this surface diff --git a/.claude/skills/searchable-events/references/searchable-kinds.md b/.claude/skills/searchable-events/references/searchable-kinds.md index 92688128ec..d9942a6530 100644 --- a/.claude/skills/searchable-events/references/searchable-kinds.md +++ b/.claude/skills/searchable-events/references/searchable-kinds.md @@ -5,7 +5,7 @@ expression. **Update this file in the same PR as any change to the searchable se `indexableContent()` body** (see SKILL.md). Verified against the code 2026-09-17. Counts: 137 concrete classes covering 140 kind values (`GitStatusEvent` spans 4 kinds; -kind 30063 has a collision — see the footnote). File paths are under +kind 30063 is shared by two NIPs — see the footnote). File paths are under `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/`. Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`. @@ -74,10 +74,10 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`. | 12473 | BirdexEvent | experimental/birdstar | `(listOfNotNull(summary()) + speciesNames())` NL | | 15128 | RootSiteEvent | nip5aStaticWebsites | `listOfNotNull(title(), description())` NL | | 15129 | RootNappletEvent | nip5dNapplets | `listOfNotNull(title(), description())` NL | -| 30000 | FollowSetEvent | nip51Lists/peopleList | `listOfNotNull(titleOrName(), description())` NL | +| 30000 | FollowSetEvent | nip51Lists/followSet | `listOfNotNull(titleOrName(), description())` NL | | 30001 | OldBookmarkListEvent | nip51Lists/bookmarkList | `listOfNotNull(title())` NL | | 30002 | RelaySetEvent | nip51Lists/relaySets | `listOfNotNull(title(), description())` NL | -| 30003 | BookmarkSetEvent | nip51Lists/labeledBookmarkList | `listOfNotNull(titleOrName(), description())` NL | +| 30003 | BookmarkSetEvent | nip51Lists/bookmarkSet | `listOfNotNull(titleOrName(), description())` NL | | 30004 | ArticleCurationSetEvent | nip51Lists/articleCurationSet | `listOfNotNull(title(), description())` NL | | 30005 | VideoCurationSetEvent | nip51Lists/videoCurationSet | `listOfNotNull(title(), description())` NL | | 30006 | PictureCurationSetEvent | nip51Lists/pictureCurationSet | `listOfNotNull(title(), description())` NL | @@ -91,7 +91,7 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`. | 30030 | EmojiPackEvent | nip30CustomEmoji/pack | `listOfNotNull(titleOrName(), description(), content)` NL | | 30054 | Podcasting20EpisodeEvent | nipXXPodcasting20/episode | `(listOfNotNull(title(), description(), content) + topics())` NL | | 30055 | Podcasting20TrailerEvent | nipXXPodcasting20/trailer | `listOfNotNull(title(), content)` NL | -| 30063 | ReleaseArtifactSetEvent † | nip51Lists/releaseArtifactSet | `listOfNotNull(title(), description())` NL | +| 30063 | ReleaseArtifactSetEvent † | nip51Lists/releaseArtifactSet | `listOfNotNull(title(), description(), content if NIP-82)` NL | | 30175 | PersonaEvent | buzz/apPersonas | `personaOrNull()?.let { listOfNotNull(it.displayName, it.systemPrompt).joinToString("\n") } ?: ""` | | 30176 | TeamEvent | buzz/teams | `teamOrNull()?.let { listOfNotNull(it.name, it.description, it.instructions).joinToString("\n") } ?: ""` | | 30177 | ManagedAgentEvent | buzz/managedAgents | `agentOrNull()?.let { listOfNotNull(it.name, it.systemPrompt).joinToString("\n") } ?: ""` | @@ -138,7 +138,7 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`. | 38192 | Ps1SaveEvent | experimental/ps1saves | `listOfNotNull(summary(), saveTitle(), region(), filename())` NL | | 38383 | P2POrderEvent | nip69P2pOrderEvents | `(listOfNotNull(makerName(), currency()) + paymentMethods().orEmpty()).joinToString(" ")` (SP) | | 39000 | GroupMetadataEvent | nip29RelayGroups/metadata | `listOfNotNull(name(), about())` NL | -| 39089 | StarterPackEvent | nip51Lists/followList | `listOfNotNull(title(), description())` NL | +| 39089 | StarterPackEvent | nip51Lists/starterPack | `listOfNotNull(title(), description())` NL | | 39092 | MediaStarterPackEvent | nip51Lists/mediaStarterPack | `listOfNotNull(title(), description())` NL | | 39307 | TextTrackEvent | nip71Video/textTrack | `content` (the WebVTT cue text) | | 39701 | WebBookmarkEvent | nipB0WebBookmarks | `listOfNotNull(title(), description())` NL | @@ -150,10 +150,10 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`. | 45003 | ForumCommentEvent | buzz/forum | `content` | | 48106 | HuddleGuidelinesEvent | buzz/huddles | `content` | -† **Kind 30063 collision:** `experimental/nip82SoftwareApps/release/SoftwareReleaseEvent` also -declares `KIND = 30063` and implements `SearchableEvent` (`content`), but `EventFactory` maps -30063 to `ReleaseArtifactSetEvent`, so on every store path kind 30063 indexes -`title()\ndescription()`. If the factory mapping ever changes, this table changes with it. +† **Kind 30063 is shared** by NIP-51 release artifact sets and NIP-82 software releases, and +`ReleaseArtifactSetEvent` parses both. It indexes `title()` and `description()`, plus `content` +(the release notes) only when the event carries the NIP-82 `i` + `version` tags — a NIP-51 set +may hold encrypted private items in `content`, which must never be indexed. ## Abstract bases (no kind of their own) diff --git a/amethyst/plans/2026-07-01-auth-permission-architecture.md b/amethyst/plans/2026-07-01-auth-permission-architecture.md index f3d431047a..17dce367c7 100644 --- a/amethyst/plans/2026-07-01-auth-permission-architecture.md +++ b/amethyst/plans/2026-07-01-auth-permission-architecture.md @@ -68,7 +68,7 @@ The NIP-42 plumbing and a first-cut permission gate are already in place: | Settings screen (global policy + per-relay list) | `amethyst/.../ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt` | | Global policy setting, persisted local-only | `AccountSettings.defaultRelayAuthPolicy`, `LocalPreferences` key `DEFAULT_RELAY_AUTH_POLICY` | | Blocked-relay list (kind 10006) | `amethyst/.../model/nip51Lists/blockedRelays/BlockedRelayListState.kt` (`.flow`) | -| Follow checks | `Account.isFollowing(...)`, `Account.allFollows.flow.value.authors`, `FollowListsState.isUserInFollowSets(...)` | +| Follow checks | `Account.isFollowing(...)`, `Account.allFollows.flow.value.authors`, `StarterPacksState.isUserInFollowSets(...)` | | DM / NIP-65 relay lookups | `DmRelayListState`, `Nip65RelayListState` (+ per-user via `LocalCache`) | ## The gap @@ -180,7 +180,7 @@ class RelayAuthContext(val relayUrl: String, val purposes: List<AuthPurpose>) - `IF_IN_MY_LIST` → `ALLOW` if relay ∈ my relay lists, else fall through - `TRUSTED_FOLLOWS` *(new — see idea A below)* → `ALLOW` if relay ∈ my lists **or** any counterparty in `ctx.purposes` is followed (`Account.allFollows` - / `FollowListsState.isUserInFollowSets`) and the purpose permits it; else + / `StarterPacksState.isUserInFollowSets`) and the purpose permits it; else fall through. 4. **Fall-through**: `ASK` if the purpose is attributable (we can show a reason); otherwise `DENY` silently (don't prompt for anonymous stranger challenges). diff --git a/amethyst/plans/2026-07-07-nip29-armada-study.md b/amethyst/plans/2026-07-07-nip29-armada-study.md index 4d8e0aa3cf..7c430631de 100644 --- a/amethyst/plans/2026-07-07-nip29-armada-study.md +++ b/amethyst/plans/2026-07-07-nip29-armada-study.md @@ -392,11 +392,11 @@ can be a group event if it carries `h` and the relay accepts the kind." has: - `metadata/` — `GroupMetadataEvent` (kind **39000**), `GroupAdminsEvent` - (39001), `GroupMembersEvent` (39002), `SupportedRolesEvent` (39003). -- `moderation/` — `CreateGroupEvent` (9007), `EditMetadataEvent` (9002), - `PutUserEvent` (9000), `RemoveUserEvent` (9001), `DeleteEventEvent` (9005), - `DeleteGroupEvent` (9008), `CreateInviteEvent` (9009), plus tag helpers. -- `request/` — `JoinRequestEvent` (9021), `LeaveRequestEvent` (9022). + (39001), `GroupMembersEvent` (39002), `GroupRolesEvent` (39003). +- `moderation/` — `CreateGroupEvent` (9007), `GroupEditMetadataEvent` (9002), + `GroupPutUserEvent` (9000), `GroupRemoveUserEvent` (9001), `GroupDeleteEventEvent` (9005), + `DeleteGroupEvent` (9008), `GroupCreateInviteEvent` (9009), plus tag helpers. +- `request/` — `GroupJoinRequestEvent` (9021), `GroupLeaveRequestEvent` (9022). - `tags/` — `GroupIdTag` (the `h` tag), `CodeTag`, `GroupAdminTag`, `RoleTag`, and even a `PreviousTag`. @@ -427,7 +427,7 @@ NIP-04/17 DMs, NIP-28 public channels, and NIP-C7 ephemeral chats — all under ### 5.2 The main protocol gap There is **no kind-9 group chat message event** in `nip29RelayGroups/` -(`CreateInviteEvent` at 9009 is the highest kind present; nothing for kind 9). +(`GroupCreateInviteEvent` at 9009 is the highest kind present; nothing for kind 9). The 9xxx moderation, 39xxx metadata, and 9021/9022 request events exist, but the actual message carrier does not. This is the first thing to build: diff --git a/amethyst/plans/2026-07-13-chat-scroll-performance.md b/amethyst/plans/2026-07-13-chat-scroll-performance.md index 4b066c6858..dd7de42179 100644 --- a/amethyst/plans/2026-07-13-chat-scroll-performance.md +++ b/amethyst/plans/2026-07-13-chat-scroll-performance.md @@ -71,7 +71,7 @@ Feed-level: marker update (only the newest visible timestamp matters; the marker is monotonic). One coroutine per scroll session instead of one per row. 6. **Jumbo without a coroutine (#4)**: only launch the decrypt effect for - encrypted kinds (`PrivateDmEvent`, sealed rumors not yet in the decrypt + encrypted kinds (`EncryptedDmEvent`, sealed rumors not yet in the decrypt cache). Plaintext kinds (public chats, NIP-17 rumors already unwrapped — the vast majority) take the synchronous path only. 7. **Retire settled delivery ticks (#6)**: once a message is fully accepted (or diff --git a/amethyst/plans/2026-07-15-bitchat-geohash-interop.md b/amethyst/plans/2026-07-15-bitchat-geohash-interop.md index 2722de432e..09af8f3a48 100644 --- a/amethyst/plans/2026-07-15-bitchat-geohash-interop.md +++ b/amethyst/plans/2026-07-15-bitchat-geohash-interop.md @@ -55,7 +55,7 @@ Bitchat's Nostr side has two chat features. Amethyst is a pure-Nostr client 1. **Encrypted DMs (Phase 2).** Port the `bitchat1:` binary packet (`BitchatPacket` TLV + `NoisePayloadType`) into quartz, wrap/unwrap it in the - existing NIP-17 stack (`GiftWrapEvent`/`SealedRumorEvent`/`ChatMessageEvent`), + existing NIP-17 stack (`GiftWrapEvent`/`SealEvent`/`ChatMessageEvent`), handle geohash DMs (to a per-geohash pubkey) and delivery/read receipts. Add `amy geochat dm` for interop testing. 2. **Desktop UI.** The shared pieces (quartz events, `GeoRelayDirectory`) are diff --git a/amethyst/plans/2026-07-23-push-notification-redesign.md b/amethyst/plans/2026-07-23-push-notification-redesign.md index 2e0acac758..a2f8d535ee 100644 --- a/amethyst/plans/2026-07-23-push-notification-redesign.md +++ b/amethyst/plans/2026-07-23-push-notification-redesign.md @@ -178,10 +178,10 @@ This keeps the *policy* (who gets notified) exactly where it is and isolates the | Kind(s) | Category | Style | Accent / icon | Title → Body | Actions | | --- | --- | --- | --- | --- | --- | -| NIP-17 `ChatMessageEvent` (14), file (15), NIP-04 `PrivateDmEvent` (4), Marmot group | DIRECT_MESSAGE | `MessagingStyle` + Conversation shortcut + Bubble | blue-green / `chat` | sender → message | Reply, Mark-read | +| NIP-17 `ChatMessageEvent` (14), file (15), NIP-04 `EncryptedDmEvent` (4), Marmot group | DIRECT_MESSAGE | `MessagingStyle` + Conversation shortcut + Bubble | blue-green / `chat` | sender → message | Reply, Mark-read | | `TextNoteEvent`(1)/`CommentEvent`(1111)/`ChannelMessageEvent`(42) **reply to me** | REPLY | `MessagingStyle` (parent as prior message) | purple / `reply` | "X replied" → excerpt (+ quoted parent) | Reply, View thread, Mute thread | | Same kinds, **mention/quote only** | MENTION | plain (BigText) | purple / `alternate_email` | "X mentioned you" → excerpt | View, Mute thread | -| `LnZapEvent`(9735) | ZAP | **colorized** amount card (BigText) | gold / `bolt` | "⚡ 2,100 sats from X" → zap comment + zapped-note excerpt | Zap back, View | +| `ZapReceiptEvent`(9735) | ZAP | **colorized** amount card (BigText) | gold / `bolt` | "⚡ 2,100 sats from X" → zap comment + zapped-note excerpt | Zap back, View | | `NutzapEvent`(9321) *(new)* | ZAP | colorized amount card | gold / `bolt` (cashu tint) | "🥜 X nutzapped you 2,100 sats" → … | Zap back, View | | `OnchainZapEvent`(8333) *(new)* | ZAP | colorized amount card | gold / `bolt` | "⛓ X sent an onchain zap" → … | View | | `ReactionEvent`(7) | REACTION | aggregated `InboxStyle` | heart-red / `favorite` | "🤙 X & N others liked your post" → note excerpt | Like back, View | @@ -189,7 +189,7 @@ This keeps the *policy* (who gets notified) exactly where it is and isolates the | `PictureEvent`(20)/`VideoNormal`/`Short`/`Vertical`/`Horizontal` | MEDIA | `BigPictureStyle` | purple / `image` | "X shared a photo/video" → caption, image inline | View | | `PollEvent`/`ZapPollEvent` | MENTION | plain | purple / `ballot` | "X asked a question" → poll title | Vote, View | | `HighlightEvent`(9802) | ARTICLE | plain | purple / `format_quote` | "X highlighted your article" → highlighted text | View | -| `LongTextNoteEvent`(30023)/`WikiNoteEvent`(30818) | ARTICLE | plain | purple / `article` | "X mentioned you in an article" → title | View | +| `LongFormContentEvent`(30023)/`WikiArticleEvent`(30818) | ARTICLE | plain | purple / `article` | "X mentioned you in an article" → title | View | | `GitIssueEvent`/`GitPatchEvent`/`GitPullRequestEvent`/`…Update` | CODE | plain | slate / `merge` | "X opened an issue/PR" → subject | View | | `BadgeAwardEvent`(8) *(new)* | BADGE | `BigPictureStyle` (badge art) | gold / `award_star` | "You earned a badge" → badge name, image | View | | `LiveChessGameAcceptEvent`/`LiveChessMoveEvent` | CHESS | plain | brown / `chess` | "X accepted your challenge" / "your turn" | Open board | diff --git a/amethyst/plans/2026-07-24-nwc-bolt12-pay.md b/amethyst/plans/2026-07-24-nwc-bolt12-pay.md index e42b00a729..4f4e0ba89a 100644 --- a/amethyst/plans/2026-07-24-nwc-bolt12-pay.md +++ b/amethyst/plans/2026-07-24-nwc-bolt12-pay.md @@ -59,7 +59,7 @@ and dispatches the decrypted `Response`, so a new method needs no changes there: decrypt-on-arrival. - `NwcPaymentTracker` (`commons/…/service/nwc/`) — request↔response match with the author-spoof gate. -- `LocalCache.consume(LnZapPaymentResponseEvent)` — routes 23195 back to the callback. +- `LocalCache.consume(NwcResponseEvent)` — routes 23195 back to the callback. - Wallet storage: `AccountSettings.nwcWallets` + `defaultPaymentSourceId`; `PaymentSourceResolver`. - Pay rail entry: `ZapPaymentHandler.zap()` → `payViaNWC()` (the `PaymentSource.Nwc` diff --git a/amethyst/plans/2026-07-31-shortpost-notify-list-picker.md b/amethyst/plans/2026-07-31-shortpost-notify-list-picker.md index e7a382b5d3..0e8cfe0552 100644 --- a/amethyst/plans/2026-07-31-shortpost-notify-list-picker.md +++ b/amethyst/plans/2026-07-31-shortpost-notify-list-picker.md @@ -41,9 +41,9 @@ storm. |---|---| | The chip row + "Add" chip | `ui/note/creators/notify/Notifying.kt` (`Notifying`, `NotifyUserChip`, `AddUserChip`) | | Audience state | `ShortNotePostViewModel.pTags: List<User>?`, `mutedNotifies: Set<HexKey>`, `activeNotifies()`, `addToReplyList(user)` | -| My NIP-51 people lists (kind 30000, public **and** decrypted private members) | `account.peopleLists.uiListFlow: StateFlow<List<PeopleList>>` (`model/nip51Lists/peopleList/PeopleListsState.kt`) | -| My follow packs (kind 39089, public members) | `account.followLists.uiListFlow` (`model/nip51Lists/peopleList/FollowListsState.kt`) | -| `PeopleList` UI model (`title`, `image`, `publicMembers`, `privateMembers` as `Set<User>`) | `model/nip51Lists/peopleList/PeopleList.kt` | +| My NIP-51 people lists (kind 30000, public **and** decrypted private members) | `account.peopleLists.uiListFlow: StateFlow<List<PeopleList>>` (`model/nip51Lists/followSets/FollowSetsState.kt`) | +| My follow packs (kind 39089, public members) | `account.followLists.uiListFlow` (`model/nip51Lists/followSets/StarterPacksState.kt`) | +| `PeopleList` UI model (`title`, `image`, `publicMembers`, `privateMembers` as `Set<User>`) | `model/nip51Lists/followSets/PeopleList.kt` | | Two-column list catalog rendering | `ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserView.kt` — same "Follow sets" + "Discover follows" sectioning | | Multi-select member review (count header, select-all checkbox, per-user row, confirm button) | `ui/screen/loggedIn/newUser/ImportFollowListPickFollowsScreen.kt` (`PreviewList` / `FollowEntryRow`) | | Bottom-sheet picker shell w/ search field | `ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupParentPicker.kt` | diff --git a/amethyst/plans/2026-09-10-dvm-heartbeat-liveness-plan.md b/amethyst/plans/2026-09-10-dvm-heartbeat-liveness-plan.md index dfe5996c2d..d56b7fd2e5 100644 --- a/amethyst/plans/2026-09-10-dvm-heartbeat-liveness-plan.md +++ b/amethyst/plans/2026-09-10-dvm-heartbeat-liveness-plan.md @@ -281,7 +281,7 @@ In `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt` ```kotlin import com.vitorpamplona.quartz.nip90Dvms.dvmHeartbeat.DvmHeartbeatEvent ``` -2. Add a dispatch branch inside the `when (kind)` near the other NIP-90 kinds (next to line 736 `NIP90StatusEvent.KIND -> ...`): +2. Add a dispatch branch inside the `when (kind)` near the other NIP-90 kinds (next to line 736 `DvmStatusEvent.KIND -> ...`): ```kotlin DvmHeartbeatEvent.KIND -> DvmHeartbeatEvent(id, pubKey, createdAt, tags, content, sig) ``` @@ -304,7 +304,7 @@ private val knownDTagReaders = - [ ] **Step 6: Update the design doc §2** -In `amethyst/plans/2026-09-10-dvm-heartbeat-liveness.md`, replace the §2 bullet that says the class "extends `BaseReplaceableEvent`" / "Overrides `dTag()`" with the as-built wording: extends `BaseAddressableEvent` (the codebase convention for 10xxx events with real `d` tags, e.g. `FollowListEvent`), so `dTag()`/`address()`/`addressTag()` come from the base; note `MAX_AGE_SECONDS`, `isFreshAt`, and the `knownDTagReaders` entry live in quartz too (commons imports them). +In `amethyst/plans/2026-09-10-dvm-heartbeat-liveness.md`, replace the §2 bullet that says the class "extends `BaseReplaceableEvent`" / "Overrides `dTag()`" with the as-built wording: extends `BaseAddressableEvent` (the codebase convention for 10xxx events with real `d` tags, e.g. `StarterPackEvent`), so `dTag()`/`address()`/`addressTag()` come from the base; note `MAX_AGE_SECONDS`, `isFreshAt`, and the `knownDTagReaders` entry live in quartz too (commons imports them). - [ ] **Step 7: Run the tests to verify they pass** diff --git a/amethyst/plans/2026-09-10-dvm-heartbeat-liveness.md b/amethyst/plans/2026-09-10-dvm-heartbeat-liveness.md index b67a5cd4bc..3e76ed1eb6 100644 --- a/amethyst/plans/2026-09-10-dvm-heartbeat-liveness.md +++ b/amethyst/plans/2026-09-10-dvm-heartbeat-liveness.md @@ -45,7 +45,7 @@ feeds and the detail surface show an offline state instead. New `quartz/.../nip90Dvms/dvmHeartbeat/DvmHeartbeatEvent.kt`: - `class DvmHeartbeatEvent(...) : BaseAddressableEvent(...)`, `KIND = 11998` — the codebase - convention for 10xxx events with real `d` tags (e.g. `FollowListEvent`), so `dTag()` / + convention for 10xxx events with real `d` tags (e.g. `StarterPackEvent`), so `dTag()` / `address()` / `addressTag()` come from the base. The cache address is `Address(11998, dvmPubkey, dTag)`, the exact mirror of the announcement's `Address(31990, dvmPubkey, dTag)`. diff --git a/amethyst/plans/2026-09-17-nip-cc-geocaching.md b/amethyst/plans/2026-09-17-nip-cc-geocaching.md index 837d0de01c..ea2a24df5a 100644 --- a/amethyst/plans/2026-09-17-nip-cc-geocaching.md +++ b/amethyst/plans/2026-09-17-nip-cc-geocaching.md @@ -83,7 +83,7 @@ The listing event is mostly existing tags wearing a new kind number: | `r` | `nip51Lists/tags/RelayTag` (`"r"`) | | `a` | `nip01Core/tags/aTag/ATag` (curation list + found log) | | `title`, `description` (37517) | `nip51Lists/tags/TitleTag`, `.../DescriptionTag` | -| embedded 7517 | `Event.fromJson(...)` (same trick `LnZapEvent.zapRequest()` uses) | +| embedded 7517 | `Event.fromJson(...)` (same trick `ZapReceiptEvent.zapRequest()` uses) | | signature check | `Event.verifySignature()` in `nip01Core/crypto/EventExt.kt` | | kind 1111 logs | `nip22Comments/CommentEvent` **as-is** — it already models `A/K/P` + `a/k/p` addressable root+parent (`rootAddress()`, `replyAddress()`, `hasRootAddress()`). Only the `t` log-type vocabulary is new. | | required-tag gate | `containsAllTagNamesWithValues(REQUIRED_FIELDS)`, as `ClassifiedsEvent.isWellFormed()` does | diff --git a/amethyst/plans/archive/2026-05-14-onchain-zaps.md b/amethyst/plans/archive/2026-05-14-onchain-zaps.md index 4e06e084b3..9a449c7120 100644 --- a/amethyst/plans/archive/2026-05-14-onchain-zaps.md +++ b/amethyst/plans/archive/2026-05-14-onchain-zaps.md @@ -94,7 +94,7 @@ Section labels: "Bitcoin" and "Lightning". ## Subscription edits — extend existing kind lists No new assemblers. Add `OnchainZapEvent.KIND` (8333) to the existing -`LnZapEvent.KIND` (9735) sites: +`ZapReceiptEvent.KIND` (9735) sites: | File | Edit | |---|---| @@ -109,7 +109,7 @@ No new assemblers. Add `OnchainZapEvent.KIND` (8333) to the existing ## Display path — fold into `Note.zapsAmount` -Today: `LocalCache.consume(LnZapEvent)` → `Note.addZap()` → `Note.updateZapTotal()` +Today: `LocalCache.consume(ZapReceiptEvent)` → `Note.addZap()` → `Note.updateZapTotal()` sums lightning amounts into `Note.zapsAmount`, which `ReactionsRow` / `ObserveZapAmountText` / `SlidingAnimationAmount` render. We add onchain zap sats to the same `Note.zapsAmount` — no UI changes required. @@ -118,7 +118,7 @@ sats to the same `Note.zapsAmount` — no UI changes required. - Add `var onchainZaps = mapOf<...>()` (separate map from `zaps`). - Extend `updateZapTotal()` to add **verified** onchain sats. Unverified or pending tx amounts are NOT counted. -- `amethyst/.../model/LocalCache.kt` (after the `consume(LnZapEvent)` block ~line 1667) +- `amethyst/.../model/LocalCache.kt` (after the `consume(ZapReceiptEvent)` block ~line 1667) - New `consume(event: OnchainZapEvent)` handler. - Reject self-zap. - Enqueue verification against the configured `OnchainBackend`. diff --git a/amethyst/plans/archive/2026-06-22-napplet-nsite-security.md b/amethyst/plans/archive/2026-06-22-napplet-nsite-security.md index d9a14ff27d..d6236690cf 100644 --- a/amethyst/plans/archive/2026-06-22-napplet-nsite-security.md +++ b/amethyst/plans/archive/2026-06-22-napplet-nsite-security.md @@ -34,7 +34,7 @@ and what remains as future work. 2. **Private mute/block leak via `identity.getMutes`/`getBlocked`.** These read `muteList.flow` / `blockPeopleList.flow`, which contain **decrypted private** entries. Now they read the events' **public** tags only (`MuteListEvent.publicMutes()`, - `PeopleListEvent.publicUsersIdSet()`). + `FollowSetEvent.publicUsersIdSet()`). 3. **Silent "allow-always" actions + UI-redress.** Added persistent **trusted chrome** (a sandbox bar the applet can't draw over: shield + name + tap-to-see "what it can diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 7f1b0da472..7a55e9b22c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -62,7 +62,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent @@ -599,7 +599,7 @@ object LocalPreferences { putOrRemove(PrefKeys.LATEST_NIP65_RELAY_LIST, settings.backupNIP65RelayList) putOrRemove(PrefKeys.LATEST_SEARCH_RELAY_LIST, settings.backupSearchRelayList) putOrRemove(PrefKeys.LATEST_INDEX_RELAY_LIST, settings.backupIndexRelayList) - putOrRemove(PrefKeys.LATEST_RELAY_FEEDS_LIST, settings.backupRelayFeedsList) + putOrRemove(PrefKeys.LATEST_RELAY_FEEDS_LIST, settings.backupFavoriteRelayList) putOrRemove(PrefKeys.LATEST_BLOCKED_RELAY_LIST, settings.backupBlockedRelayList) putOrRemove(PrefKeys.LATEST_TRUSTED_RELAY_LIST, settings.backupTrustedRelayList) @@ -613,9 +613,9 @@ object LocalPreferences { putOrRemove(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST, settings.backupPrivateHomeRelayList) putOrRemove(PrefKeys.LATEST_APP_SPECIFIC_DATA, settings.backupAppSpecificData) - putOrRemove(PrefKeys.LATEST_CHANNEL_LIST, settings.backupChannelList) + putOrRemove(PrefKeys.LATEST_CHANNEL_LIST, settings.backupPublicChatList) putOrRemove(PrefKeys.LATEST_COMMUNITY_LIST, settings.backupCommunityList) - putOrRemove(PrefKeys.LATEST_HASHTAG_LIST, settings.backupHashtagList) + putOrRemove(PrefKeys.LATEST_HASHTAG_LIST, settings.backupInterestList) putOrRemove(PrefKeys.LATEST_GEOHASH_LIST, settings.backupGeohashList) putOrRemove(PrefKeys.LATEST_EPHEMERAL_LIST, settings.backupEphemeralChatList) putOrRemove(PrefKeys.LATEST_RELAY_GROUP_LIST, settings.backupRelayGroupList) @@ -827,15 +827,15 @@ object LocalPreferences { val latestNip65RelayListStr = getString(PrefKeys.LATEST_NIP65_RELAY_LIST, null) val latestSearchRelayListStr = getString(PrefKeys.LATEST_SEARCH_RELAY_LIST, null) val latestIndexRelayListStr = getString(PrefKeys.LATEST_INDEX_RELAY_LIST, null) - val latestRelayFeedsListStr = getString(PrefKeys.LATEST_RELAY_FEEDS_LIST, null) + val latestFavoriteRelayListStr = getString(PrefKeys.LATEST_RELAY_FEEDS_LIST, null) val latestBlockedRelayListStr = getString(PrefKeys.LATEST_BLOCKED_RELAY_LIST, null) val latestTrustedRelayListStr = getString(PrefKeys.LATEST_TRUSTED_RELAY_LIST, null) val latestMuteListStr = getString(PrefKeys.LATEST_MUTE_LIST, null) val latestPrivateHomeRelayListStr = getString(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST, null) val latestAppSpecificDataStr = getString(PrefKeys.LATEST_APP_SPECIFIC_DATA, null) - val latestChannelListStr = getString(PrefKeys.LATEST_CHANNEL_LIST, null) + val latestPublicChatListStr = getString(PrefKeys.LATEST_CHANNEL_LIST, null) val latestCommunityListStr = getString(PrefKeys.LATEST_COMMUNITY_LIST, null) - val latestHashtagListStr = getString(PrefKeys.LATEST_HASHTAG_LIST, null) + val latestInterestListStr = getString(PrefKeys.LATEST_HASHTAG_LIST, null) val latestGeohashListStr = getString(PrefKeys.LATEST_GEOHASH_LIST, null) val latestEphemeralListStr = getString(PrefKeys.LATEST_EPHEMERAL_LIST, null) val latestRelayGroupListStr = getString(PrefKeys.LATEST_RELAY_GROUP_LIST, null) @@ -891,15 +891,15 @@ object LocalPreferences { val latestNip65RelayList = async { parseEventOrNull<AdvertisedRelayListEvent>(latestNip65RelayListStr) } val latestSearchRelayList = async { parseEventOrNull<SearchRelayListEvent>(latestSearchRelayListStr) } val latestIndexRelayList = async { parseEventOrNull<IndexerRelayListEvent>(latestIndexRelayListStr) } - val latestRelayFeedsList = async { parseEventOrNull<FavoriteRelayListEvent>(latestRelayFeedsListStr) } + val latestFavoriteRelayList = async { parseEventOrNull<FavoriteRelayListEvent>(latestFavoriteRelayListStr) } val latestBlockedRelayList = async { parseEventOrNull<BlockedRelayListEvent>(latestBlockedRelayListStr) } val latestTrustedRelayList = async { parseEventOrNull<TrustedRelayListEvent>(latestTrustedRelayListStr) } val latestMuteList = async { parseEventOrNull<MuteListEvent>(latestMuteListStr) } val latestPrivateHomeRelayList = async { parseEventOrNull<PrivateOutboxRelayListEvent>(latestPrivateHomeRelayListStr) } val latestAppSpecificData = async { parseEventOrNull<AppSpecificDataEvent>(latestAppSpecificDataStr) } - val latestChannelList = async { parseEventOrNull<PublicChatListEvent>(latestChannelListStr) } + val latestPublicChatList = async { parseEventOrNull<PublicChatListEvent>(latestPublicChatListStr) } val latestCommunityList = async { parseEventOrNull<CommunityListEvent>(latestCommunityListStr) } - val latestHashtagList = async { parseEventOrNull<InterestListEvent>(latestHashtagListStr) } + val latestInterestList = async { parseEventOrNull<InterestListEvent>(latestInterestListStr) } val latestGeohashList = async { parseEventOrNull<GeohashListEvent>(latestGeohashListStr) } val latestEphemeralList = async { parseEventOrNull<EphemeralChatListEvent>(latestEphemeralListStr) } val latestRelayGroupList = async { parseEventOrNull<SimpleGroupListEvent>(latestRelayGroupListStr) } @@ -947,15 +947,15 @@ object LocalPreferences { val latestNip65RelayListResolved = latestNip65RelayList.await() val latestSearchRelayListResolved = latestSearchRelayList.await() val latestIndexRelayListResolved = latestIndexRelayList.await() - val latestRelayFeedsListResolved = latestRelayFeedsList.await() + val latestFavoriteRelayListResolved = latestFavoriteRelayList.await() val latestBlockedRelayListResolved = latestBlockedRelayList.await() val latestTrustedRelayListResolved = latestTrustedRelayList.await() val latestMuteListResolved = latestMuteList.await() val latestPrivateHomeRelayListResolved = latestPrivateHomeRelayList.await() val latestAppSpecificDataResolved = latestAppSpecificData.await() - val latestChannelListResolved = latestChannelList.await() + val latestPublicChatListResolved = latestPublicChatList.await() val latestCommunityListResolved = latestCommunityList.await() - val latestHashtagListResolved = latestHashtagList.await() + val latestInterestListResolved = latestInterestList.await() val latestGeohashListResolved = latestGeohashList.await() val latestEphemeralListResolved = latestEphemeralList.await() val latestRelayGroupListResolved = latestRelayGroupList.await() @@ -1049,15 +1049,15 @@ object LocalPreferences { backupDMRelayList = latestDmRelayListResolved, backupSearchRelayList = latestSearchRelayListResolved, backupIndexRelayList = latestIndexRelayListResolved, - backupRelayFeedsList = latestRelayFeedsListResolved, + backupFavoriteRelayList = latestFavoriteRelayListResolved, backupBlockedRelayList = latestBlockedRelayListResolved, backupTrustedRelayList = latestTrustedRelayListResolved, backupPrivateHomeRelayList = latestPrivateHomeRelayListResolved, backupMuteList = latestMuteListResolved, backupAppSpecificData = latestAppSpecificDataResolved, - backupChannelList = latestChannelListResolved, + backupPublicChatList = latestPublicChatListResolved, backupCommunityList = latestCommunityListResolved, - backupHashtagList = latestHashtagListResolved, + backupInterestList = latestInterestListResolved, backupGeohashList = latestGeohashListResolved, backupEphemeralChatList = latestEphemeralListResolved, backupRelayGroupList = latestRelayGroupListResolved, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 438be42d4f..b69ea5f2b1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -75,21 +75,21 @@ import com.vitorpamplona.amethyst.commons.model.nip51Lists.blockPeopleList.Block import com.vitorpamplona.amethyst.commons.model.nip51Lists.blockedRelays.BlockedRelayListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip51Lists.broadcastRelays.BroadcastRelayListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip51Lists.favoriteAlgoFeedsLists.FavoriteAlgoFeedsListDecryptionCache +import com.vitorpamplona.amethyst.commons.model.nip51Lists.favoriteRelays.FavoriteRelayListDecryptionCache +import com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets.FollowSetDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip51Lists.geohashLists.GeohashListDecryptionCache -import com.vitorpamplona.amethyst.commons.model.nip51Lists.hashtagLists.HashtagListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip51Lists.indexerRelays.IndexerRelayListDecryptionCache +import com.vitorpamplona.amethyst.commons.model.nip51Lists.interestLists.InterestListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip51Lists.muteList.MuteListDecryptionCache -import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip51Lists.proxyRelays.ProxyRelayListDecryptionCache -import com.vitorpamplona.amethyst.commons.model.nip51Lists.relayFeeds.RelayFeedsListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip51Lists.searchRelays.SearchRelayListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip51Lists.trustedRelays.TrustedRelayListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip56Reports.ReportAction import com.vitorpamplona.amethyst.commons.model.nip62Vanish.VanishRequestsState import com.vitorpamplona.amethyst.commons.model.nip72Communities.CommunityListDecryptionCache -import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.ContactCardDecryptionCache -import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.ContactCardsState import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.TrustProviderListDecryptionCache +import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.UserAssertionDecryptionCache +import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.UserAssertionsState import com.vitorpamplona.amethyst.commons.model.nipBCOnchainZaps.OnchainWalletState import com.vitorpamplona.amethyst.commons.model.privateChatLastReadRoute import com.vitorpamplona.amethyst.commons.model.privateChats.hasEncryptedContent @@ -145,18 +145,18 @@ import com.vitorpamplona.amethyst.model.nip46Signer.Nip46SignerState import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.bookmarkSets.BookmarkSetsState import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.favoriteAlgoFeedsLists.FavoriteAlgoFeedsListState +import com.vitorpamplona.amethyst.model.nip51Lists.favoriteRelays.FavoriteRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSetsState +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.StarterPacksState import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState -import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListState import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.interestLists.InterestListState import com.vitorpamplona.amethyst.model.nip51Lists.interestSets.InterestSetsState -import com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkListsState import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListState -import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.FollowListsState -import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListsState import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState -import com.vitorpamplona.amethyst.model.nip51Lists.relayFeeds.RelayFeedListState import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState @@ -281,8 +281,8 @@ import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportType -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.PrivateZapCache +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup @@ -639,8 +639,8 @@ class Account( val indexerRelayListDecryptionCache = IndexerRelayListDecryptionCache(signer) val indexerRelayList = IndexerRelayListState(signer, cache, indexerRelayListDecryptionCache, scope, settings) - val relayFeedsListDecryptionCache = RelayFeedsListDecryptionCache(signer) - val relayFeedsList = RelayFeedListState(signer, cache, relayFeedsListDecryptionCache, scope, settings) + val favoriteRelayListDecryptionCache = FavoriteRelayListDecryptionCache(signer) + val favoriteRelayList = FavoriteRelayListState(signer, cache, favoriteRelayListDecryptionCache, scope, settings) val blockedRelayListDecryptionCache = BlockedRelayListDecryptionCache(signer) val blockedRelayList = BlockedRelayListState(signer, cache, blockedRelayListDecryptionCache, scope, settings) @@ -758,8 +758,8 @@ class Account( val communityListDecryptionCache = CommunityListDecryptionCache(signer) val communityList = CommunityListState(signer, cache, communityListDecryptionCache, scope, settings) - val hashtagListDecryptionCache = HashtagListDecryptionCache(signer) - val hashtagList = HashtagListState(signer, cache, hashtagListDecryptionCache, scope, settings) + val interestListDecryptionCache = InterestListDecryptionCache(signer) + val interestList = InterestListState(signer, cache, interestListDecryptionCache, scope, settings) val favoriteAlgoFeedsListDecryptionCache = FavoriteAlgoFeedsListDecryptionCache(signer) val favoriteAlgoFeedsList = FavoriteAlgoFeedsListState(signer, cache, favoriteAlgoFeedsListDecryptionCache, scope, settings) @@ -777,14 +777,14 @@ class Account( val trustProviderListDecryptionCache = TrustProviderListDecryptionCache(signer) val trustProviderList = TrustProviderListState(signer, cache, trustProviderListDecryptionCache, scope, settings) - val peopleListDecryptionCache = PeopleListDecryptionCache(signer) - val blockPeopleList = BlockPeopleListState(signer, cache, peopleListDecryptionCache, scope) - val peopleLists = PeopleListsState(signer, cache, peopleListDecryptionCache, scope) - val followLists = FollowListsState(signer, cache, scope) + val followSetDecryptionCache = FollowSetDecryptionCache(signer) + val blockPeopleList = BlockPeopleListState(signer, cache, followSetDecryptionCache, scope) + val followSets = FollowSetsState(signer, cache, followSetDecryptionCache, scope) + val starterPacks = StarterPacksState(signer, cache, scope) val hiddenUsers = HiddenUsersState(muteList.flow, blockPeopleList.flow, scope, settings) - val labeledBookmarkLists = LabeledBookmarkListsState(signer, cache, scope) + val bookmarkSets = BookmarkSetsState(signer, cache, scope) val interestSets = InterestSetsState(signer, cache, scope) val appRecommendations = AppRecommendationsState(signer, cache, scope) val oldBookmarkState = OldBookmarkListState(signer, cache, scope) @@ -795,8 +795,8 @@ class Account( val ownedEmojiPacks = OwnedEmojiPacksState(signer, cache, scope) // needs `emoji` above: nickname edits resolve :shortcodes: against the account's packs - val contactCardDecryptionCache = ContactCardDecryptionCache(signer) - val contactCards = ContactCardsState(signer, cache, contactCardDecryptionCache, emoji) + val userAssertionDecryptionCache = UserAssertionDecryptionCache(signer) + val userAssertions = UserAssertionsState(signer, cache, userAssertionDecryptionCache, emoji) val vanish = VanishRequestsState(signer, cache, client, scope) @@ -893,7 +893,7 @@ class Account( val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, proxyRelayList, cache, scope).flow // Merges all follow lists to create a single All Follows feed. - val allFollows = MergedFollowListsState(kind3FollowList, peopleLists, followLists, hashtagList, geohashList, communityList, scope) + val allFollows = MergedFollowListsState(kind3FollowList, followSets, starterPacks, interestList, geohashList, communityList, scope) val privateDMDecryptionCache = PrivateDMCache(signer) override val privateZapsDecryptionCache = PrivateZapCache(signer) @@ -1030,10 +1030,10 @@ class Account( val feedDecryptionCaches = FeedDecryptionCaches( - peopleListCache = peopleListDecryptionCache, + peopleListCache = followSetDecryptionCache, muteListCache = muteListDecryptionCache, communityListCache = communityListDecryptionCache, - hashtagCache = hashtagListDecryptionCache, + hashtagCache = interestListDecryptionCache, geohashCache = geohashListDecryptionCache, ) @@ -1047,7 +1047,7 @@ class Account( blockedRelays = blockedRelayList.flow, proxyRelays = proxyRelayList.flow, mineRelays = mineRelays.flow, - relayFeeds = relayFeedsList.flow, + relayFeeds = favoriteRelayList.flow, caches = feedDecryptionCaches, signer = signer, scope = scope, @@ -2126,9 +2126,9 @@ class Account( suspend fun unfollow(community: AddressableNote) = sendMyPublicAndPrivateOutbox(communityList.unfollow(community)) - suspend fun followHashtag(tag: String) = sendMyPublicAndPrivateOutbox(hashtagList.follow(tag)) + suspend fun followHashtag(tag: String) = sendMyPublicAndPrivateOutbox(interestList.follow(tag)) - suspend fun unfollowHashtag(tag: String) = sendMyPublicAndPrivateOutbox(hashtagList.unfollow(tag)) + suspend fun unfollowHashtag(tag: String) = sendMyPublicAndPrivateOutbox(interestList.unfollow(tag)) suspend fun followFavoriteAlgoFeed(dvm: AddressBookmark) = sendMyPublicAndPrivateOutbox(favoriteAlgoFeedsList.follow(dvm)) @@ -3440,11 +3440,11 @@ class Account( * encrypted in the content. `null` clears a field. Goes out through the * account's extended outbox relays. */ - suspend fun updateContactCardPetName( + suspend fun updateUserAssertionPetName( pubkeyHex: HexKey, petName: String?, summary: String?, - ) = sendMyPublicAndPrivateOutbox(contactCards.updatePetNameAndSummary(pubkeyHex, petName, summary)) + ) = sendMyPublicAndPrivateOutbox(userAssertions.updatePetNameAndSummary(pubkeyHex, petName, summary)) suspend fun showUser(pubkeyHex: HexKey) { sendMyPublicAndPrivateOutbox(blockPeopleList.showUser(pubkeyHex)) @@ -3550,7 +3550,7 @@ class Account( } } - suspend fun decryptZapOrNull(event: ZapRequestEvent): LnZapPrivateEvent? = if (event.isPrivateZap() && isWriteable()) privateZapsDecryptionCache.decryptPrivateZap(event) else null + suspend fun decryptZapOrNull(event: ZapRequestEvent): PrivateZapEvent? = if (event.isPrivateZap() && isWriteable()) privateZapsDecryptionCache.decryptPrivateZap(event) else null fun isAllHidden(users: Set<HexKey>): Boolean = users.all { isHidden(it) } @@ -3747,11 +3747,11 @@ class Account( suspend fun saveTrustedRelayList(trustedRelays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(trustedRelayList.saveRelayList(trustedRelays)) - suspend fun saveRelayFeedsList(trustedRelays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(relayFeedsList.saveRelayList(trustedRelays)) + suspend fun saveFavoriteRelayList(trustedRelays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(favoriteRelayList.saveRelayList(trustedRelays)) - suspend fun followRelayFeed(url: NormalizedRelayUrl) = sendMyPublicAndPrivateOutbox(relayFeedsList.addRelay(url)) + suspend fun followRelayFeed(url: NormalizedRelayUrl) = sendMyPublicAndPrivateOutbox(favoriteRelayList.addRelay(url)) - suspend fun unfollowRelayFeed(url: NormalizedRelayUrl) = sendMyPublicAndPrivateOutbox(relayFeedsList.removeRelay(url)) + suspend fun unfollowRelayFeed(url: NormalizedRelayUrl) = sendMyPublicAndPrivateOutbox(favoriteRelayList.removeRelay(url)) suspend fun saveBlockedRelayList(blockedRelays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(blockedRelayList.saveRelayList(blockedRelays)) @@ -3783,7 +3783,7 @@ class Account( proxyRelayList.getProxyRelayList(), broadcastRelayList.getBroadcastRelayList(), indexerRelayList.getIndexerRelayList(), - relayFeedsList.getRelayFeedsList(), + favoriteRelayList.getFavoriteRelayList(), blockedRelayList.getBlockedRelayList(), muteList.getMuteList(), bookmarkState.getBookmarkList(), @@ -4056,9 +4056,9 @@ class Account( logTime("Account ${userProfile().toBestDisplayName()} newEventBundle Update with ${newNotes.size} new notes") { upgradeAttestations() newNotesPreProcessor.runNew(newNotes) - peopleLists.newNotes(newNotes) - followLists.newNotes(newNotes) - labeledBookmarkLists.newNotes(newNotes) + followSets.newNotes(newNotes) + starterPacks.newNotes(newNotes) + bookmarkSets.newNotes(newNotes) interestSets.newNotes(newNotes) ownedEmojiPacks.newNotes(newNotes) } @@ -4069,9 +4069,9 @@ class Account( cache.live.deletedEventBundles.collect { deletedNotes -> logTime("Account ${userProfile().toBestDisplayName()} deletedEventBundle Update with ${deletedNotes.size} new notes") { newNotesPreProcessor.runDeleted(deletedNotes) - peopleLists.deletedNotes(deletedNotes) - followLists.deletedNotes(deletedNotes) - labeledBookmarkLists.deletedNotes(deletedNotes) + followSets.deletedNotes(deletedNotes) + starterPacks.deletedNotes(deletedNotes) + bookmarkSets.deletedNotes(deletedNotes) interestSets.deletedNotes(deletedNotes) ownedEmojiPacks.deletedNotes(deletedNotes) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index b70197390c..b6e63d83e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -68,7 +68,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent @@ -208,15 +208,15 @@ class AccountSettings( var backupNIP65RelayList: AdvertisedRelayListEvent? = null, var backupSearchRelayList: SearchRelayListEvent? = null, var backupIndexRelayList: IndexerRelayListEvent? = null, - var backupRelayFeedsList: FavoriteRelayListEvent? = null, + var backupFavoriteRelayList: FavoriteRelayListEvent? = null, var backupBlockedRelayList: BlockedRelayListEvent? = null, var backupTrustedRelayList: TrustedRelayListEvent? = null, var backupMuteList: MuteListEvent? = null, var backupPrivateHomeRelayList: PrivateOutboxRelayListEvent? = null, var backupAppSpecificData: AppSpecificDataEvent? = null, - var backupChannelList: PublicChatListEvent? = null, + var backupPublicChatList: PublicChatListEvent? = null, var backupCommunityList: CommunityListEvent? = null, - var backupHashtagList: InterestListEvent? = null, + var backupInterestList: InterestListEvent? = null, var backupFavoriteAlgoFeedsList: FavoriteAlgoFeedsListEvent? = null, var backupGeohashList: GeohashListEvent? = null, var backupEphemeralChatList: EphemeralChatListEvent? = null, @@ -1084,13 +1084,13 @@ class AccountSettings( is Bolt12OfferListEvent -> updateBolt12Offers(incoming) is SearchRelayListEvent -> updateSearchRelayList(incoming) is IndexerRelayListEvent -> updateIndexRelayList(incoming) - is FavoriteRelayListEvent -> updateRelayFeedList(incoming) + is FavoriteRelayListEvent -> updateFavoriteRelayList(incoming) is BlockedRelayListEvent -> updateBlockedRelayList(incoming) is TrustedRelayListEvent -> updateTrustedRelayList(incoming) is PrivateOutboxRelayListEvent -> updatePrivateHomeRelayList(incoming) - is PublicChatListEvent -> updateChannelListTo(incoming) + is PublicChatListEvent -> updatePublicChatListTo(incoming) is GeohashListEvent -> updateGeohashListTo(incoming) - is InterestListEvent -> updateHashtagListTo(incoming) + is InterestListEvent -> updateInterestListTo(incoming) is FavoriteAlgoFeedsListEvent -> updateFavoriteAlgoFeedsListTo(incoming) is CommunityListEvent -> updateCommunityListTo(incoming) is EphemeralChatListEvent -> updateEphemeralChatListTo(incoming) @@ -1284,11 +1284,11 @@ class AccountSettings( } } - fun updateRelayFeedList(newRelayFeedList: FavoriteRelayListEvent?) { - if (newRelayFeedList == null) return + fun updateFavoriteRelayList(newFavoriteRelayList: FavoriteRelayListEvent?) { + if (newFavoriteRelayList == null) return - if (backupGuard.accept(backupRelayFeedsList, newRelayFeedList, isEmpty = newRelayFeedList.tags.isEmpty()) { updateRelayFeedList(newRelayFeedList) }) { - backupRelayFeedsList = newRelayFeedList + if (backupGuard.accept(backupFavoriteRelayList, newFavoriteRelayList, isEmpty = newFavoriteRelayList.tags.isEmpty()) { updateFavoriteRelayList(newFavoriteRelayList) }) { + backupFavoriteRelayList = newFavoriteRelayList saveAccountSettings() } } @@ -1320,13 +1320,13 @@ class AccountSettings( } } - override fun channelList() = backupChannelList + override fun publicChatList() = backupPublicChatList - override fun updateChannelListTo(newChannelList: PublicChatListEvent?) { - if (newChannelList == null) return + override fun updatePublicChatListTo(newPublicChatList: PublicChatListEvent?) { + if (newPublicChatList == null) return - if (backupGuard.accept(backupChannelList, newChannelList, isEmpty = newChannelList.tags.isEmpty()) { updateChannelListTo(newChannelList) }) { - backupChannelList = newChannelList + if (backupGuard.accept(backupPublicChatList, newPublicChatList, isEmpty = newPublicChatList.tags.isEmpty()) { updatePublicChatListTo(newPublicChatList) }) { + backupPublicChatList = newPublicChatList saveAccountSettings() } } @@ -1340,11 +1340,11 @@ class AccountSettings( } } - fun updateHashtagListTo(newHashtagList: InterestListEvent?) { - if (newHashtagList == null) return + fun updateInterestListTo(newInterestList: InterestListEvent?) { + if (newInterestList == null) return - if (backupGuard.accept(backupHashtagList, newHashtagList, isEmpty = newHashtagList.tags.isEmpty()) { updateHashtagListTo(newHashtagList) }) { - backupHashtagList = newHashtagList + if (backupGuard.accept(backupInterestList, newInterestList, isEmpty = newInterestList.tags.isEmpty()) { updateInterestListTo(newInterestList) }) { + backupInterestList = newInterestList saveAccountSettings() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/EventBroadcaster.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/EventBroadcaster.kt index 9765753489..c5a8d6737c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/EventBroadcaster.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/EventBroadcaster.kt @@ -45,7 +45,7 @@ import com.vitorpamplona.quartz.nip29RelayGroups.isGroupScoped import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.BookmarkSetEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/bookmarkSets/BookmarkSetsState.kt similarity index 80% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/bookmarkSets/BookmarkSetsState.kt index 49a1c8b3d4..305f9c26e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/bookmarkSets/BookmarkSetsState.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.nip51Lists.labeledBookmarkLists +package com.vitorpamplona.amethyst.model.nip51Lists.bookmarkSets import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filter import com.vitorpamplona.amethyst.commons.model.eventIdSet import com.vitorpamplona.amethyst.commons.model.events -import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList +import com.vitorpamplona.amethyst.commons.model.nip51Lists.bookmarkSets.BookmarkSet import com.vitorpamplona.amethyst.commons.model.updateFlow import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.nip01Core.core.Address @@ -37,10 +37,10 @@ import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.description -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.image -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.title +import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.BookmarkSetEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.description +import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.image +import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.title import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -55,41 +55,41 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.flow.update -class LabeledBookmarkListsState( +class BookmarkSetsState( val signer: NostrSigner, val cache: LocalCache, val scope: CoroutineScope, ) { val user = cache.getOrCreateUser(signer.pubKey) - fun existingLabeledBookmarkNotes() = cache.addressables.filter(BookmarkSetEvent.KIND, user.pubkeyHex) + fun existingBookmarkSetNotes() = cache.addressables.filter(BookmarkSetEvent.KIND, user.pubkeyHex) - val labeledBookmarkListVersions = MutableStateFlow(0) + val bookmarkSetVersions = MutableStateFlow(0) - val labeledBookmarkListNotes = - labeledBookmarkListVersions - .map { existingLabeledBookmarkNotes() } - .onStart { emit(existingLabeledBookmarkNotes()) } + val bookmarkSetNotes = + bookmarkSetVersions + .map { existingBookmarkSetNotes() } + .onStart { emit(existingBookmarkSetNotes()) } .flowOn(Dispatchers.IO) .stateIn(scope, SharingStarted.Eagerly, emptyList()) - val labeledBookmarkListEventIds = - labeledBookmarkListNotes + val bookmarkSetEventIds = + bookmarkSetNotes .map { it.eventIdSet() } - .onStart { emit(labeledBookmarkListNotes.value.eventIdSet()) } + .onStart { emit(bookmarkSetNotes.value.eventIdSet()) } .flowOn(Dispatchers.IO) .stateIn(scope, SharingStarted.Eagerly, emptySet()) @OptIn(ExperimentalCoroutinesApi::class) val latestBookmarkLists: StateFlow<List<BookmarkSetEvent>> = - labeledBookmarkListNotes + bookmarkSetNotes .transformLatest { emitAll(it.updateFlow<BookmarkSetEvent>()) } - .onStart { emit(labeledBookmarkListNotes.value.events()) } + .onStart { emit(bookmarkSetNotes.value.events()) } .flowOn(Dispatchers.IO) .stateIn(scope, SharingStarted.Eagerly, emptyList()) - suspend fun BookmarkSetEvent.toLabeledBookmarkList() = - LabeledBookmarkList( + suspend fun BookmarkSetEvent.toBookmarkSet() = + BookmarkSet( identifier = dTag(), title = titleOrName() ?: dTag(), description = description(), @@ -98,23 +98,23 @@ class LabeledBookmarkListsState( publicBookmarks = publicBookmarks().toSet(), ) - suspend fun List<BookmarkSetEvent>.toLabeledBookmarkListsFeed() = map { it.toLabeledBookmarkList() }.sortedBy { it.title } + suspend fun List<BookmarkSetEvent>.toBookmarkSetsFeed() = map { it.toBookmarkSet() }.sortedBy { it.title } val listFeedFlow = latestBookmarkLists - .map { it.toLabeledBookmarkListsFeed() } - .onStart { emit(latestBookmarkLists.value.toLabeledBookmarkListsFeed()) } + .map { it.toBookmarkSetsFeed() } + .onStart { emit(latestBookmarkLists.value.toBookmarkSetsFeed()) } .flowOn(Dispatchers.IO) .stateIn(scope, SharingStarted.Eagerly, emptyList()) - fun List<LabeledBookmarkList>.getList(bookmarkListId: String) = + fun List<BookmarkSet>.getList(bookmarkListId: String) = this.firstOrNull { it.identifier == bookmarkListId } fun getBookmarkList(dTag: String) = listFeedFlow.value.getList(bookmarkListId = dTag) - fun DeletionRequestEvent.hasAnyDeletedBookmarkLists() = deleteAddressesWithKind(BookmarkSetEvent.KIND) || deletesAnyEventIn(labeledBookmarkListEventIds.value) + fun DeletionRequestEvent.hasAnyDeletedBookmarkLists() = deleteAddressesWithKind(BookmarkSetEvent.KIND) || deletesAnyEventIn(bookmarkSetEventIds.value) fun hasItemInNoteList(notes: Set<Note>): Boolean = notes.anyNotNullEvent { event -> @@ -138,14 +138,14 @@ class LabeledBookmarkListsState( } fun forceRefresh() { - labeledBookmarkListVersions.update { it + 1 } + bookmarkSetVersions.update { it + 1 } } - fun getLabeledBookmarkListNote(bookmarkIdentifier: String): AddressableNote? = existingLabeledBookmarkNotes().find { it.dTag() == bookmarkIdentifier } + fun getBookmarkSetNote(bookmarkIdentifier: String): AddressableNote? = existingBookmarkSetNotes().find { it.dTag() == bookmarkIdentifier } - fun getLabeledBookmarkListEvent(bookmarkIdentifier: String): BookmarkSetEvent = getLabeledBookmarkListNote(bookmarkIdentifier)?.event as BookmarkSetEvent + fun getBookmarkSetEvent(bookmarkIdentifier: String): BookmarkSetEvent = getBookmarkSetNote(bookmarkIdentifier)?.event as BookmarkSetEvent - fun getLabeledBookmarkListFlow(bookmarkIdentifier: String) = + fun getBookmarkSetFlow(bookmarkIdentifier: String) = listFeedFlow .map { it.getList(bookmarkIdentifier) } .onStart { @@ -154,7 +154,7 @@ class LabeledBookmarkListsState( ) }.flowOn(Dispatchers.IO) - suspend fun addLabeledBookmarkList( + suspend fun addBookmarkSet( listName: String, listDescription: String? = null, listImage: String? = null, @@ -178,10 +178,10 @@ class LabeledBookmarkListsState( listName: String?, listDescription: String?, listImage: String?, - bookmarkList: LabeledBookmarkList, + bookmarkList: BookmarkSet, account: Account, ) { - val listEvent = getLabeledBookmarkListEvent(bookmarkList.identifier) + val listEvent = getBookmarkSetEvent(bookmarkList.identifier) val template = listEvent.update { @@ -197,10 +197,10 @@ class LabeledBookmarkListsState( suspend fun renameBookmarkList( newName: String, - bookmarkList: LabeledBookmarkList, + bookmarkList: BookmarkSet, account: Account, ) { - val listEvent = getLabeledBookmarkListEvent(bookmarkList.identifier) + val listEvent = getBookmarkSetEvent(bookmarkList.identifier) val renamedList = BookmarkSetEvent.modifyName( earlierVersion = listEvent, @@ -212,10 +212,10 @@ class LabeledBookmarkListsState( suspend fun modifyListDescription( newDescription: String?, - bookmarkList: LabeledBookmarkList, + bookmarkList: BookmarkSet, account: Account, ) { - val listEvent = getLabeledBookmarkListEvent(bookmarkList.identifier) + val listEvent = getBookmarkSetEvent(bookmarkList.identifier) val modifiedList = BookmarkSetEvent.modifyDescription( earlierVersion = listEvent, @@ -226,7 +226,7 @@ class LabeledBookmarkListsState( } suspend fun cloneBookmarkList( - currentBookmarkList: LabeledBookmarkList, + currentBookmarkList: BookmarkSet, customCloneName: String?, customCloneDescription: String?, account: Account, @@ -246,7 +246,7 @@ class LabeledBookmarkListsState( bookmarkListIdentifier: String, account: Account, ) { - val listEvent = getLabeledBookmarkListEvent(bookmarkListIdentifier) + val listEvent = getBookmarkSetEvent(bookmarkListIdentifier) val deletionEventTemplate = DeletionRequestEvent.build(listOf(listEvent)) val deletionEvent = account.signer.sign(deletionEventTemplate) account.sendMyPublicAndPrivateOutbox(deletionEvent) @@ -258,7 +258,7 @@ class LabeledBookmarkListsState( isBookmarkPrivate: Boolean, account: Account, ) { - val currentBookmarkList = getLabeledBookmarkListEvent(bookmarkListIdentifier) + val currentBookmarkList = getBookmarkSetEvent(bookmarkListIdentifier) val updatedList = BookmarkSetEvent.addBookmark( earlierVersion = currentBookmarkList, @@ -275,7 +275,7 @@ class LabeledBookmarkListsState( isBookmarkCurrentlyPrivate: Boolean, account: Account, ) { - val bookmarkList = getLabeledBookmarkListEvent(bookmarkListIdentifier) + val bookmarkList = getBookmarkSetEvent(bookmarkListIdentifier) val updatedList = BookmarkSetEvent.moveBookmark( earlierVersion = bookmarkList, @@ -292,7 +292,7 @@ class LabeledBookmarkListsState( isBookmarkPrivate: Boolean, account: Account, ) { - val currentBookmarkList = getLabeledBookmarkListEvent(bookmarkListIdentifier) + val currentBookmarkList = getBookmarkSetEvent(bookmarkListIdentifier) val updatedList = BookmarkSetEvent.removeBookmark( earlierVersion = currentBookmarkList, @@ -311,7 +311,7 @@ class LabeledBookmarkListsState( ) { if (deletedEventIds.isEmpty() && deletedAddresses.isEmpty()) return - val currentList = getLabeledBookmarkListNote(bookmarkListIdentifier)?.event as? BookmarkSetEvent ?: return + val currentList = getBookmarkSetNote(bookmarkListIdentifier)?.event as? BookmarkSetEvent ?: return val newPublicTags = currentList.tags diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayFeeds/RelayFeedListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/favoriteRelays/FavoriteRelayListState.kt similarity index 65% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayFeeds/RelayFeedListState.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/favoriteRelays/FavoriteRelayListState.kt index 685e8411a9..2f0fa88e44 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayFeeds/RelayFeedListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/favoriteRelays/FavoriteRelayListState.kt @@ -18,12 +18,12 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.nip51Lists.relayFeeds +package com.vitorpamplona.amethyst.model.nip51Lists.favoriteRelays import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.commons.model.cache.LocalCache -import com.vitorpamplona.amethyst.commons.model.nip51Lists.relayFeeds.RelayFeedsListDecryptionCache +import com.vitorpamplona.amethyst.commons.model.nip51Lists.favoriteRelays.FavoriteRelayListDecryptionCache import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner @@ -40,32 +40,32 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -class RelayFeedListState( +class FavoriteRelayListState( val signer: NostrSigner, val cache: LocalCache, - val decryptionCache: RelayFeedsListDecryptionCache, + val decryptionCache: FavoriteRelayListDecryptionCache, val scope: CoroutineScope, val settings: AccountSettings, ) { // Creates a long-term reference for this note so that the GC doesn't collect the note it self - val relayFeedListNote = cache.getOrCreateAddressableNote(getRelayFeedsListAddress()) + val favoriteRelayListNote = cache.getOrCreateAddressableNote(getFavoriteRelayListAddress()) - fun getRelayFeedsListAddress() = FavoriteRelayListEvent.createAddress(signer.pubKey) + fun getFavoriteRelayListAddress() = FavoriteRelayListEvent.createAddress(signer.pubKey) - fun getRelayFeedsListFlow(): StateFlow<NoteState> = relayFeedListNote.flow().metadata.stateFlow + fun getFavoriteRelayListFlow(): StateFlow<NoteState> = favoriteRelayListNote.flow().metadata.stateFlow - fun getRelayFeedsList(): FavoriteRelayListEvent? = relayFeedListNote.event as? FavoriteRelayListEvent + fun getFavoriteRelayList(): FavoriteRelayListEvent? = favoriteRelayListNote.event as? FavoriteRelayListEvent - fun relayFeedsListEvent(note: Note) = note.event as? FavoriteRelayListEvent ?: settings.backupRelayFeedsList + fun favoriteRelayListEvent(note: Note) = note.event as? FavoriteRelayListEvent ?: settings.backupFavoriteRelayList - suspend fun normalizeRelayFeedsListWithBackup(note: Note): Set<NormalizedRelayUrl> = relayFeedsListEvent(note)?.let { decryptionCache.relays(it) }?.ifEmpty { null } ?: emptySet() + suspend fun normalizeFavoriteRelayListWithBackup(note: Note): Set<NormalizedRelayUrl> = favoriteRelayListEvent(note)?.let { decryptionCache.relays(it) }?.ifEmpty { null } ?: emptySet() - suspend fun normalizeRelayFeedsListWithBackupNoDefaults(note: Note): Set<NormalizedRelayUrl> = relayFeedsListEvent(note)?.let { decryptionCache.relays(it) } ?: emptySet() + suspend fun normalizeFavoriteRelayListWithBackupNoDefaults(note: Note): Set<NormalizedRelayUrl> = favoriteRelayListEvent(note)?.let { decryptionCache.relays(it) } ?: emptySet() val flow = - getRelayFeedsListFlow() - .map { normalizeRelayFeedsListWithBackup(it.note) } - .onStart { emit(normalizeRelayFeedsListWithBackup(relayFeedListNote)) } + getFavoriteRelayListFlow() + .map { normalizeFavoriteRelayListWithBackup(it.note) } + .onStart { emit(normalizeFavoriteRelayListWithBackup(favoriteRelayListNote)) } .flowOn(Dispatchers.IO) .stateIn( scope, @@ -74,9 +74,9 @@ class RelayFeedListState( ) val flowNoDefaults = - getRelayFeedsListFlow() - .map { normalizeRelayFeedsListWithBackupNoDefaults(it.note) } - .onStart { emit(normalizeRelayFeedsListWithBackupNoDefaults(relayFeedListNote)) } + getFavoriteRelayListFlow() + .map { normalizeFavoriteRelayListWithBackupNoDefaults(it.note) } + .onStart { emit(normalizeFavoriteRelayListWithBackupNoDefaults(favoriteRelayListNote)) } .flowOn(Dispatchers.IO) .stateIn( scope, @@ -85,24 +85,24 @@ class RelayFeedListState( ) suspend fun addRelay(relay: NormalizedRelayUrl): FavoriteRelayListEvent { - val current = normalizeRelayFeedsListWithBackupNoDefaults(relayFeedListNote).toMutableList() + val current = normalizeFavoriteRelayListWithBackupNoDefaults(favoriteRelayListNote).toMutableList() if (relay !in current) current.add(relay) return saveRelayList(current) } suspend fun removeRelay(relay: NormalizedRelayUrl): FavoriteRelayListEvent? { - val current = normalizeRelayFeedsListWithBackupNoDefaults(relayFeedListNote).toMutableList() + val current = normalizeFavoriteRelayListWithBackupNoDefaults(favoriteRelayListNote).toMutableList() if (relay !in current) return null current.remove(relay) return saveRelayList(current) } suspend fun saveRelayList(relayFeeds: List<NormalizedRelayUrl>): FavoriteRelayListEvent { - val relayFeedsList = getRelayFeedsList() + val favoriteRelayList = getFavoriteRelayList() - return if (relayFeedsList != null && relayFeedsList.tags.isNotEmpty()) { + return if (favoriteRelayList != null && favoriteRelayList.tags.isNotEmpty()) { FavoriteRelayListEvent.updateRelayList( - earlierVersion = relayFeedsList, + earlierVersion = favoriteRelayList, relays = relayFeeds, signer = signer, ) @@ -115,7 +115,7 @@ class RelayFeedListState( } init { - settings.backupRelayFeedsList?.let { + settings.backupFavoriteRelayList?.let { Log.d("AccountRegisterObservers") { "Loading saved relay feeds list ${it.toJson()}" } @OptIn(DelicateCoroutinesApi::class) scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } @@ -123,10 +123,10 @@ class RelayFeedListState( scope.launch(Dispatchers.IO) { Log.d("AccountRegisterObservers", "Relay feeds list Collector Start") - getRelayFeedsListFlow().collect { + getFavoriteRelayListFlow().collect { Log.d("AccountRegisterObservers") { "Updating Relay feeds list for ${signer.pubKey}" } (it.note.event as? FavoriteRelayListEvent)?.let { - settings.updateRelayFeedList(it) + settings.updateFavoriteRelayList(it) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetsState.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetsState.kt index bdd1731ab8..4b002aec38 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/FollowSetsState.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.nip51Lists.peopleList +package com.vitorpamplona.amethyst.model.nip51Lists.followSets import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note @@ -28,19 +28,19 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filter import com.vitorpamplona.amethyst.commons.model.eventIdSet import com.vitorpamplona.amethyst.commons.model.events -import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleList -import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleListDecryptionCache +import com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets.FollowSetDecryptionCache +import com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets.PeopleList import com.vitorpamplona.amethyst.commons.model.updateFlow import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.update import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent +import com.vitorpamplona.quartz.nip51Lists.followSet.description +import com.vitorpamplona.quartz.nip51Lists.followSet.image +import com.vitorpamplona.quartz.nip51Lists.followSet.title import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag -import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.description -import com.vitorpamplona.quartz.nip51Lists.peopleList.image -import com.vitorpamplona.quartz.nip51Lists.peopleList.title import com.vitorpamplona.quartz.utils.flattenToSet import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -62,10 +62,10 @@ import java.util.UUID * * This class must receive updates from Large Cache as it receives new events. */ -class PeopleListsState( +class FollowSetsState( val signer: NostrSigner, val cache: LocalCache, - val decryptionCache: PeopleListDecryptionCache, + val decryptionCache: FollowSetDecryptionCache, val scope: CoroutineScope, ) { val user = cache.getOrCreateUser(signer.pubKey) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/StarterPacksState.kt similarity index 91% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/StarterPacksState.kt index fbcdfe4e46..881b97a43d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/FollowListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/followSets/StarterPacksState.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.nip51Lists.peopleList +package com.vitorpamplona.amethyst.model.nip51Lists.followSets import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filter import com.vitorpamplona.amethyst.commons.model.eventIdSet import com.vitorpamplona.amethyst.commons.model.events -import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleList +import com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets.PeopleList import com.vitorpamplona.amethyst.commons.model.updateFlow import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -36,14 +36,14 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.update import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent -import com.vitorpamplona.quartz.nip51Lists.followList.description -import com.vitorpamplona.quartz.nip51Lists.followList.image -import com.vitorpamplona.quartz.nip51Lists.followList.person -import com.vitorpamplona.quartz.nip51Lists.followList.personFirst -import com.vitorpamplona.quartz.nip51Lists.followList.removePerson -import com.vitorpamplona.quartz.nip51Lists.followList.title import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.description +import com.vitorpamplona.quartz.nip51Lists.starterPack.image +import com.vitorpamplona.quartz.nip51Lists.starterPack.person +import com.vitorpamplona.quartz.nip51Lists.starterPack.personFirst +import com.vitorpamplona.quartz.nip51Lists.starterPack.removePerson +import com.vitorpamplona.quartz.nip51Lists.starterPack.title import com.vitorpamplona.quartz.utils.flattenToSet import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -65,7 +65,7 @@ import java.util.UUID * * This class must receive updates from Large Cache as it receives new events. */ -class FollowListsState( +class StarterPacksState( val signer: NostrSigner, val cache: LocalCache, val scope: CoroutineScope, @@ -84,25 +84,25 @@ class FollowListsState( val followListVersions = MutableStateFlow(0) - val followListNotes = + val starterPackNotes = followListVersions .map { existingPeopleListNotes() } .onStart { emit(existingPeopleListNotes()) } .flowOn(Dispatchers.IO) .stateIn(scope, SharingStarted.Eagerly, emptyList()) - val followListsEventIds = - followListNotes + val starterPacksEventIds = + starterPackNotes .map { it.eventIdSet() } - .onStart { emit(followListNotes.value.eventIdSet()) } + .onStart { emit(starterPackNotes.value.eventIdSet()) } .flowOn(Dispatchers.IO) .stateIn(scope, SharingStarted.Eagerly, emptySet()) @OptIn(ExperimentalCoroutinesApi::class) val latestLists: StateFlow<List<StarterPackEvent>> = - followListNotes + starterPackNotes .transformLatest { emitAll(it.updateFlow<StarterPackEvent>()) } - .onStart { emit(followListNotes.value.events()) } + .onStart { emit(starterPackNotes.value.events()) } .flowOn(Dispatchers.IO) .stateIn(scope, SharingStarted.Eagerly, emptyList()) @@ -148,12 +148,12 @@ class FollowListsState( fun isUserInFollowSets(user: User): Boolean = allPeopleListProfiles.value.contains(user.pubkeyHex) - fun DeletionRequestEvent.hasDeletedAnyFollowList() = deleteAddressesWithKind(StarterPackEvent.KIND) || deletesAnyEventIn(followListsEventIds.value) + fun DeletionRequestEvent.hasDeletedAnyStarterPack() = deleteAddressesWithKind(StarterPackEvent.KIND) || deletesAnyEventIn(starterPacksEventIds.value) fun hasItemInNoteList(notes: Set<Note>): Boolean = notes.anyNotNullEvent { event -> if (event.pubKey == signer.pubKey) { - event is StarterPackEvent || (event is DeletionRequestEvent && event.hasDeletedAnyFollowList()) + event is StarterPackEvent || (event is DeletionRequestEvent && event.hasDeletedAnyStarterPack()) } else { false } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/interestLists/InterestListState.kt similarity index 80% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/interestLists/InterestListState.kt index 8ef88a5e24..25dcf8712b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/interestLists/InterestListState.kt @@ -18,15 +18,15 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists +package com.vitorpamplona.amethyst.model.nip51Lists.interestLists import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.commons.model.cache.LocalCache -import com.vitorpamplona.amethyst.commons.model.nip51Lists.hashtagLists.HashtagListDecryptionCache +import com.vitorpamplona.amethyst.commons.model.nip51Lists.interestLists.InterestListDecryptionCache import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi @@ -40,30 +40,30 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch -class HashtagListState( +class InterestListState( val signer: NostrSigner, val cache: LocalCache, - val decryptionCache: HashtagListDecryptionCache, + val decryptionCache: InterestListDecryptionCache, val scope: CoroutineScope, val settings: AccountSettings, ) { // Creates a long-term reference for this note so that the GC doesn't collect the note it self - val hashtagListNote = cache.getOrCreateAddressableNote(getHashtagListAddress()) + val hashtagListNote = cache.getOrCreateAddressableNote(getInterestListAddress()) - fun getHashtagListAddress() = InterestListEvent.createAddress(signer.pubKey) + fun getInterestListAddress() = InterestListEvent.createAddress(signer.pubKey) - fun getHashtagListFlow(): StateFlow<NoteState> = hashtagListNote.flow().metadata.stateFlow + fun getInterestListFlow(): StateFlow<NoteState> = hashtagListNote.flow().metadata.stateFlow - fun getHashtagList(): InterestListEvent? = hashtagListNote.event as? InterestListEvent + fun getInterestList(): InterestListEvent? = hashtagListNote.event as? InterestListEvent suspend fun hashtagListWithBackup(note: Note): Set<String> { - val event = note.event as? InterestListEvent ?: settings.backupHashtagList + val event = note.event as? InterestListEvent ?: settings.backupInterestList return event?.let { decryptionCache.hashtags(it).mapTo(mutableSetOf()) { it.lowercase() } } ?: emptySet() } @OptIn(ExperimentalCoroutinesApi::class) val flow: StateFlow<Set<String>> = - getHashtagListFlow() + getInterestListFlow() .transformLatest { noteState -> emit(hashtagListWithBackup(noteState.note)) }.onStart { @@ -76,7 +76,7 @@ class HashtagListState( ) suspend fun follow(hashtags: List<String>): InterestListEvent { - val hashtagList = getHashtagList() + val hashtagList = getInterestList() return if (hashtagList == null) { InterestListEvent.create(hashtags, true, signer) @@ -86,7 +86,7 @@ class HashtagListState( } suspend fun follow(hashtag: String): InterestListEvent { - val hashtagList = getHashtagList() + val hashtagList = getInterestList() return if (hashtagList == null) { InterestListEvent.create(hashtag.lowercase(), true, signer) @@ -96,7 +96,7 @@ class HashtagListState( } suspend fun unfollow(hashtag: String): InterestListEvent? { - val hashtagList = getHashtagList() + val hashtagList = getInterestList() return if (hashtagList != null) { InterestListEvent.remove(hashtagList, hashtag, signer) @@ -106,7 +106,7 @@ class HashtagListState( } init { - settings.backupHashtagList?.let { event -> + settings.backupInterestList?.let { event -> Log.d("AccountRegisterObservers") { "Loading saved Hashtag list ${event.toJson()}" } @OptIn(DelicateCoroutinesApi::class) scope.launch(Dispatchers.IO) { @@ -116,10 +116,10 @@ class HashtagListState( scope.launch(Dispatchers.IO) { Log.d("AccountRegisterObservers", "Hashtag List Collector Start") - getHashtagListFlow().collect { + getInterestListFlow().collect { Log.d("AccountRegisterObservers") { "Hashtag List for ${signer.pubKey}" } (it.note.event as? InterestListEvent)?.let { - settings.updateHashtagListTo(it) + settings.updateInterestListTo(it) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountPreferenceStores.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountPreferenceStores.kt index c7fdad1616..ec7248a3b1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountPreferenceStores.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountPreferenceStores.kt @@ -49,9 +49,9 @@ class AccountPreferenceStores( val latestMuteList = stringPreferencesKey("latestMuteList") val latestPrivateHomeRelayList = stringPreferencesKey("latestPrivateHomeRelayList") val latestAppSpecificData = stringPreferencesKey("latestAppSpecificData") - val latestChannelList = stringPreferencesKey("latestChannelList") + val latestPublicChatList = stringPreferencesKey("latestChannelList") val latestCommunityList = stringPreferencesKey("latestCommunityList") - val latestHashtagList = stringPreferencesKey("latestHashtagList") + val latestInterestList = stringPreferencesKey("latestHashtagList") val latestGeohashList = stringPreferencesKey("latestGeohashList") val latestEphemeralChatList = stringPreferencesKey("latestEphemeralChatList") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt index 85a875363d..29ad3470c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.model.serverList import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.nip02FollowLists.Kind3FollowListState +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSetsState +import com.vitorpamplona.amethyst.model.nip51Lists.followSets.StarterPacksState import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState -import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListState -import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.FollowListsState -import com.vitorpamplona.amethyst.model.nip51Lists.peopleList.PeopleListsState +import com.vitorpamplona.amethyst.model.nip51Lists.interestLists.InterestListState import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip72ModCommunities.follow.tags.CommunityTag @@ -44,9 +44,9 @@ import kotlinx.coroutines.flow.stateIn @Suppress("UNCHECKED_CAST") class MergedFollowListsState( val kind3List: Kind3FollowListState, - val peopleList: PeopleListsState, - val followList: FollowListsState, - val hashtagList: HashtagListState, + val followSets: FollowSetsState, + val starterPacks: StarterPacksState, + val interestList: InterestListState, val geohashList: GeohashListState, val communityList: CommunityListState, val scope: CoroutineScope, @@ -85,9 +85,9 @@ class MergedFollowListsState( combine( listOf( kind3List.flow, - peopleList.allGoodPeopleListProfiles, - followList.allPeopleListProfiles, - hashtagList.flow, + followSets.allGoodPeopleListProfiles, + starterPacks.allPeopleListProfiles, + interestList.flow, geohashList.flow, communityList.flow, ), @@ -104,9 +104,9 @@ class MergedFollowListsState( emit( mergeLists( kind3List.flow.value, - peopleList.allGoodPeopleListProfiles.value, - followList.allPeopleListProfiles.value, - hashtagList.flow.value, + followSets.allGoodPeopleListProfiles.value, + starterPacks.allPeopleListProfiles.value, + interestList.flow.value, geohashList.flow.value, communityList.flow.value, ), @@ -118,9 +118,9 @@ class MergedFollowListsState( SharingStarted.Eagerly, mergeLists( kind3List.flow.value, - peopleList.allGoodPeopleListProfiles.value, - followList.allPeopleListProfiles.value, - hashtagList.flow.value, + followSets.allGoodPeopleListProfiles.value, + starterPacks.allPeopleListProfiles.value, + interestList.flow.value, geohashList.flow.value, communityList.flow.value, ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationDispatcher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationDispatcher.kt index e9bca8f358..b7f3b0908b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationDispatcher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationDispatcher.kt @@ -101,7 +101,7 @@ class NotificationDispatcher( private const val TAG = "NotificationDispatcher" // The dispatcher observes the *final* notification payload kinds. - // GiftWrap/EphemeralGiftWrap (1059/21059) and SealedRumor (13) are NOT + // GiftWrap/EphemeralGiftWrap (1059/21059) and Seal (13) are NOT // listed here — by the time we care, Account.newNotesPreProcessor has // already unwrapped them and inserted the inner payload into LocalCache, // which fires the observer a second time on the inner event. @@ -235,8 +235,8 @@ class NotificationDispatcher( if (event is WakeUpEvent) return@predicate true // getNoteIfExists(event) — not (event.id) — so - // AddressableEvent kinds (LongTextNote, WikiNote, - // LiveChess*, VideoHorizontal/Vertical) resolve to + // AddressableEvent kinds (LongFormContent, WikiArticle, + // LiveChess*, AddressableNormalVideo/AddressableShortVideo) resolve to // their address-keyed replaceable note. The id-keyed // version note has its replyTo moved away during // insertion (LocalCache.consumeBaseReplaceable), so diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PushWrapDecryptor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PushWrapDecryptor.kt index 17964dbecd..356051c867 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PushWrapDecryptor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/PushWrapDecryptor.kt @@ -36,7 +36,7 @@ import kotlinx.coroutines.CancellationException * * Once decrypted, the inner event is fed into [LocalCache]. From there the * normal `Account.newNotesPreProcessor` → `GiftWrapEventHandler` → - * `SealedRumorEventHandler` chain unwraps any remaining layers, and + * `SealEventHandler` chain unwraps any remaining layers, and * [NotificationDispatcher] picks up the final payload and notifies. */ object PushWrapDecryptor { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt index 75a9090d82..a4403f5659 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata -import com.vitorpamplona.amethyst.commons.relayClient.assemblers.filterContactCardsByAuthorInTheRelay +import com.vitorpamplona.amethyst.commons.relayClient.assemblers.filterUserAssertionsByAuthorInTheRelay import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState.Companion.APP_SPECIFIC_DATA_D_TAG @@ -37,7 +37,7 @@ import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayList import com.vitorpamplona.quartz.nip38UserStatus.UserStatusEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent @@ -141,7 +141,7 @@ fun filterAccountInfoAndListsFromKey( ), // The account's own kind:30382 contact cards (nicknames, NIP-44 encrypted). // Addressable — one card per target user — hence its own larger-limit filter. - filterContactCardsByAuthorInTheRelay( + filterUserAssertionsByAuthorInTheRelay( relay = relay, authors = pubkeys, since = since, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index bd26b5ad5a..ba253f5585 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -39,7 +39,7 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -64,11 +64,11 @@ fun observeUserName( // Subscribe in the relay for changes in the metadata of this user. UserFinderFilterAssemblerSubscription(user, accountViewModel) - val contactCards = accountViewModel.account.contactCards - val flow = remember(user) { contactCards.displayNameFlow(user) } + val userAssertions = accountViewModel.account.userAssertions + val flow = remember(user) { userAssertions.displayNameFlow(user) } // Subscribe in the LocalCache for changes that arrive in the device - return flow.collectAsStateWithLifecycle(remember(user) { contactCards.cachedDisplayName(user) }) + return flow.collectAsStateWithLifecycle(remember(user) { userAssertions.cachedDisplayName(user) }) } /** @@ -83,10 +83,10 @@ fun observeUserNickname( user: User, accountViewModel: AccountViewModel, ): State<Nickname?> { - val contactCards = accountViewModel.account.contactCards - val flow = remember(user) { contactCards.nicknameFlow(user) } + val userAssertions = accountViewModel.account.userAssertions + val flow = remember(user) { userAssertions.nicknameFlow(user) } - return flow.collectAsStateWithLifecycle(remember(user) { contactCards.cachedNickname(user) }) + return flow.collectAsStateWithLifecycle(remember(user) { userAssertions.cachedNickname(user) }) } @Composable @@ -132,9 +132,9 @@ fun observeUserTagFollowCount( .metadata.stateFlow .sample(1000) .mapLatest { noteState -> - (noteState.note.event as? InterestListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.size ?: 0 + (noteState.note.event as? InterestListEvent)?.let { accountViewModel.account.interestListDecryptionCache.hashtags(it) }?.size ?: 0 }.onStart { - emit((accountViewModel.hashtagFollows(user).event as? InterestListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.size ?: 0) + emit((accountViewModel.hashtagFollows(user).event as? InterestListEvent)?.let { accountViewModel.account.interestListDecryptionCache.hashtags(it) }?.size ?: 0) }.distinctUntilChanged() .flowOn(Dispatchers.IO) } @@ -160,9 +160,9 @@ fun observeUserTagFollows( .metadata.stateFlow .sample(200) .mapLatest { noteState -> - (noteState.note.event as? InterestListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.sorted() ?: emptyList() + (noteState.note.event as? InterestListEvent)?.let { accountViewModel.account.interestListDecryptionCache.hashtags(it) }?.sorted() ?: emptyList() }.onStart { - emit((accountViewModel.hashtagFollows(user).event as? InterestListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.sorted() ?: emptyList()) + emit((accountViewModel.hashtagFollows(user).event as? InterestListEvent)?.let { accountViewModel.account.interestListDecryptionCache.hashtags(it) }?.sorted() ?: emptyList()) }.distinctUntilChanged() .flowOn(Dispatchers.IO) } @@ -304,16 +304,16 @@ fun observeUserIsFollowingHashtag( // Subscribe in the LocalCache for changes that arrive in the device val flow = remember(accountViewModel) { - accountViewModel.account.hashtagList.flow + accountViewModel.account.interestList.flow .mapLatest { hashtags -> hashtag in hashtags }.onStart { - emit(hashtag in accountViewModel.account.hashtagList.flow.value) + emit(hashtag in accountViewModel.account.interestList.flow.value) }.distinctUntilChanged() .flowOn(Dispatchers.IO) } - return flow.collectAsStateWithLifecycle(hashtag in accountViewModel.account.hashtagList.flow.value) + return flow.collectAsStateWithLifecycle(hashtag in accountViewModel.account.interestList.flow.value) } @SuppressLint("StateFlowValueCalledInComposition") @@ -373,16 +373,16 @@ fun observeUserIsFollowingRelay( ): State<Boolean> { val flow = remember(accountViewModel) { - accountViewModel.account.relayFeedsList.flowNoDefaults + accountViewModel.account.favoriteRelayList.flowNoDefaults .mapLatest { relays -> relayUrl in relays }.onStart { - emit(relayUrl in accountViewModel.account.relayFeedsList.flowNoDefaults.value) + emit(relayUrl in accountViewModel.account.favoriteRelayList.flowNoDefaults.value) }.distinctUntilChanged() .flowOn(Dispatchers.IO) } - return flow.collectAsStateWithLifecycle(relayUrl in accountViewModel.account.relayFeedsList.flowNoDefaults.value) + return flow.collectAsStateWithLifecycle(relayUrl in accountViewModel.account.favoriteRelayList.flowNoDefaults.value) } @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) @@ -439,7 +439,7 @@ fun observeUserIsFollowingChannel( @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) @Composable -fun observeUserContactCardsScore( +fun observeUserAssertionsScore( user: User, accountViewModel: AccountViewModel, subscribe: Boolean = true, @@ -459,7 +459,7 @@ fun observeUserContactCardsScore( @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) @Composable -fun observeUserContactCardsFollowerCount( +fun observeUserAssertionsFollowerCount( user: User, accountViewModel: AccountViewModel, ): State<String> { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/MeteringNostrSigner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/MeteringNostrSigner.kt index 1b5355efef..8bfe2fe129 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/MeteringNostrSigner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/resourceusage/MeteringNostrSigner.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip46RemoteSigner.signer.NostrSignerRemote import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip89AppHandlers.clientTag.NostrSignerWithClientTag @@ -89,7 +89,7 @@ class MeteringNostrSigner( fromPublicKey: HexKey, ): String = metered(UsageKeys.DECRYPT_COUNT, UsageKeys.DECRYPT_US) { inner.nip44Decrypt(ciphertext, fromPublicKey) } - override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = metered(UsageKeys.DECRYPT_COUNT, UsageKeys.DECRYPT_US) { inner.decryptZapEvent(event) } + override suspend fun decryptZapEvent(event: ZapRequestEvent): PrivateZapEvent = metered(UsageKeys.DECRYPT_COUNT, UsageKeys.DECRYPT_US) { inner.decryptZapEvent(event) } override suspend fun deriveKey(nonce: HexKey): HexKey = inner.deriveKey(nonce) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt index 79d22a9dbe..9715816619 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt @@ -32,8 +32,8 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasMoreHashtagsThan import com.vitorpamplona.quartz.nip10Notes.threadRootIdOrSelf +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.utils.TimeUtils class FilterByListParams( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/PrefetchFeedMedia.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/PrefetchFeedMedia.kt index 3d70cf26d2..c13a8124a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/PrefetchFeedMedia.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/PrefetchFeedMedia.kt @@ -303,7 +303,7 @@ private class WarmTargets { * `m` mime type (a hashed URL may carry no extension). The decode listener * records their dims like any other image. */ - fun addFileHeaderMedia(event: Event) { + fun addFileMetadataMedia(event: Event) { val urls = event.tags.mapNotNull(UrlTag::parse) if (urls.isEmpty()) return val imageMime = event.tags.firstNotNullOfOrNull(MimeTypeTag::parse)?.startsWith("image/") == true @@ -335,7 +335,7 @@ private fun Note.collectWarmTargets(): WarmTargets? { // NIP-94 file-header convention: the blob is a top-level `url` tag with sibling // `m`/`dim`, not imeta (FileMetadataEvent and the gallery/file kinds). The generic // imetas() above misses these entirely. - targets.addFileHeaderMedia(ev) + targets.addFileMetadataMedia(ev) // NIP-73 external-content scope. Only kind 1111 carries one, so the type check // keeps every other kind out of the tag walk. Reuses scope() — the same accessor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt index a500753e1b..f7ad534111 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt @@ -146,7 +146,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.profileContentHeaderModifier import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsFollowerCount +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAssertionsFollowerCount import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses import com.vitorpamplona.amethyst.ui.layouts.PermanentDrawerWidth @@ -587,7 +587,7 @@ fun DisplayFollowerCount( baseAccountUser: Account, accountViewModel: AccountViewModel, ) { - val followerCount by observeUserContactCardsFollowerCount(baseAccountUser.userProfile(), accountViewModel) + val followerCount by observeUserAssertionsFollowerCount(baseAccountUser.userProfile(), accountViewModel) Text( text = followerCount, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt index 3962bce3ac..198ff3adac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt @@ -57,7 +57,7 @@ import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestEvent import com.vitorpamplona.quartz.nip34Git.pr.GitPullRequestUpdateEvent import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 12c29afce3..a227d40fcb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -106,7 +106,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.bitcoinColor import com.vitorpamplona.amethyst.commons.ui.theme.overPictureBackground import com.vitorpamplona.amethyst.commons.ui.theme.profile35dpModifier import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsScore +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAssertionsScore import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture import com.vitorpamplona.amethyst.ui.components.CoreSecretMessage import com.vitorpamplona.amethyst.ui.components.ExpandableRichTextViewer @@ -841,7 +841,7 @@ fun ObserveAndRenderBoxedUserCards( accountViewModel: AccountViewModel, subscribe: Boolean = true, ) { - val score by observeUserContactCardsScore(user, accountViewModel, subscribe) + val score by observeUserAssertionsScore(user, accountViewModel, subscribe) score?.let { Box(modifier = Size35Modifier, contentAlignment = Alignment.BottomCenter) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index a05755b51f..29134ae6da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -140,18 +140,18 @@ import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayBroadcastRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayContactList import com.vitorpamplona.amethyst.ui.note.types.DisplayDMRelayList -import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList +import com.vitorpamplona.amethyst.ui.note.types.DisplayFavoriteRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayIndexerRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayPeopleList import com.vitorpamplona.amethyst.ui.note.types.DisplayProxyRelayList -import com.vitorpamplona.amethyst.ui.note.types.DisplayRelayFeedsList import com.vitorpamplona.amethyst.ui.note.types.DisplayRelaySet import com.vitorpamplona.amethyst.ui.note.types.DisplaySearchRelayList +import com.vitorpamplona.amethyst.ui.note.types.DisplayStarterPack import com.vitorpamplona.amethyst.ui.note.types.DisplayTrustedRelayList import com.vitorpamplona.amethyst.ui.note.types.EditState import com.vitorpamplona.amethyst.ui.note.types.EmptyState -import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay +import com.vitorpamplona.amethyst.ui.note.types.FileMetadataDisplay import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay import com.vitorpamplona.amethyst.ui.note.types.PictureDisplay import com.vitorpamplona.amethyst.ui.note.types.RenderAppDefinition @@ -180,6 +180,8 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderCitation import com.vitorpamplona.amethyst.ui.note.types.RenderClassifieds import com.vitorpamplona.amethyst.ui.note.types.RenderCommunity import com.vitorpamplona.amethyst.ui.note.types.RenderCyberspaceBag +import com.vitorpamplona.amethyst.ui.note.types.RenderDvmContentDiscoveryResponse +import com.vitorpamplona.amethyst.ui.note.types.RenderDvmStatus import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack import com.vitorpamplona.amethyst.ui.note.types.RenderEntityRating import com.vitorpamplona.amethyst.ui.note.types.RenderExternalReaction @@ -200,14 +202,11 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityChatMessage import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityEvent import com.vitorpamplona.amethyst.ui.note.types.RenderLiveChessChallenge import com.vitorpamplona.amethyst.ui.note.types.RenderLiveChessGameEnd -import com.vitorpamplona.amethyst.ui.note.types.RenderLnZap import com.vitorpamplona.amethyst.ui.note.types.RenderLongFormContent import com.vitorpamplona.amethyst.ui.note.types.RenderMeetingRoomEvent import com.vitorpamplona.amethyst.ui.note.types.RenderMeetingSpaceEvent import com.vitorpamplona.amethyst.ui.note.types.RenderMusicPlaylist import com.vitorpamplona.amethyst.ui.note.types.RenderMusicTrack -import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90ContentDiscoveryResponse -import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90Status import com.vitorpamplona.amethyst.ui.note.types.RenderNamedNappletEvent import com.vitorpamplona.amethyst.ui.note.types.RenderNamedSiteEvent import com.vitorpamplona.amethyst.ui.note.types.RenderNipContent @@ -257,21 +256,22 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderWikiMergeAcceptance import com.vitorpamplona.amethyst.ui.note.types.RenderWikiMergeRequest import com.vitorpamplona.amethyst.ui.note.types.RenderWikiRedirect import com.vitorpamplona.amethyst.ui.note.types.RenderZapPoll +import com.vitorpamplona.amethyst.ui.note.types.RenderZapReceipt import com.vitorpamplona.amethyst.ui.note.types.ReplyRenderType import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay import com.vitorpamplona.amethyst.ui.note.types.lists.RenderAppCurationSet import com.vitorpamplona.amethyst.ui.note.types.lists.RenderArticleCurationSet import com.vitorpamplona.amethyst.ui.note.types.lists.RenderBookmarkList +import com.vitorpamplona.amethyst.ui.note.types.lists.RenderBookmarkSet import com.vitorpamplona.amethyst.ui.note.types.lists.RenderFavoriteAlgoFeedsList import com.vitorpamplona.amethyst.ui.note.types.lists.RenderGeohashList import com.vitorpamplona.amethyst.ui.note.types.lists.RenderGitAuthorList import com.vitorpamplona.amethyst.ui.note.types.lists.RenderGitRepositoryList import com.vitorpamplona.amethyst.ui.note.types.lists.RenderGoodWikiAuthorList import com.vitorpamplona.amethyst.ui.note.types.lists.RenderGoodWikiRelayList -import com.vitorpamplona.amethyst.ui.note.types.lists.RenderHashtagList +import com.vitorpamplona.amethyst.ui.note.types.lists.RenderInterestList import com.vitorpamplona.amethyst.ui.note.types.lists.RenderInterestSet import com.vitorpamplona.amethyst.ui.note.types.lists.RenderKindMuteSet -import com.vitorpamplona.amethyst.ui.note.types.lists.RenderLabeledBookmarkList import com.vitorpamplona.amethyst.ui.note.types.lists.RenderMediaFollowList import com.vitorpamplona.amethyst.ui.note.types.lists.RenderMediaStarterPack import com.vitorpamplona.amethyst.ui.note.types.lists.RenderMuteList @@ -367,21 +367,20 @@ import com.vitorpamplona.quartz.nip51Lists.appCurationSet.AppCurationSetEvent import com.vitorpamplona.quartz.nip51Lists.articleCurationSet.ArticleCurationSetEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.BookmarkSetEvent import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent import com.vitorpamplona.quartz.nip51Lists.gitAuthorList.GitAuthorListEvent import com.vitorpamplona.quartz.nip51Lists.gitRepositoryList.GitRepositoryListEvent import com.vitorpamplona.quartz.nip51Lists.goodWikiAuthorList.GoodWikiAuthorListEvent import com.vitorpamplona.quartz.nip51Lists.goodWikiRelayList.GoodWikiRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent import com.vitorpamplona.quartz.nip51Lists.kindMuteSet.KindMuteSetEvent -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent import com.vitorpamplona.quartz.nip51Lists.mediaFollowList.MediaFollowListEvent import com.vitorpamplona.quartz.nip51Lists.mediaStarterPack.MediaStarterPackEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.pictureCurationSet.PictureCurationSetEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent @@ -392,6 +391,7 @@ import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip51Lists.videoCurationSet.VideoCurationSetEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent @@ -1192,7 +1192,7 @@ private fun RenderNoteRow( } is ZapReceiptEvent -> { - RenderLnZap(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) + RenderZapReceipt(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) } is NutzapEvent -> { @@ -1220,7 +1220,7 @@ private fun RenderNoteRow( } is StarterPackEvent -> { - DisplayFollowList(baseNote, true, accountViewModel, nav) + DisplayStarterPack(baseNote, true, accountViewModel, nav) } is ContactListEvent -> { @@ -1252,7 +1252,7 @@ private fun RenderNoteRow( } is FavoriteRelayListEvent -> { - DisplayRelayFeedsList(baseNote, backgroundColor, accountViewModel, nav) + DisplayFavoriteRelayList(baseNote, backgroundColor, accountViewModel, nav) } is IndexerRelayListEvent -> { @@ -1551,7 +1551,7 @@ private fun RenderNoteRow( } is DvmContentDiscoveryResponseEvent -> { - RenderNIP90ContentDiscoveryResponse( + RenderDvmContentDiscoveryResponse( baseNote, makeItShort, canPreview, @@ -1563,7 +1563,7 @@ private fun RenderNoteRow( } is DvmStatusEvent -> { - RenderNIP90Status( + RenderDvmStatus( baseNote, accountViewModel, nav, @@ -1641,7 +1641,7 @@ private fun RenderNoteRow( } is FileMetadataEvent -> { - FileHeaderDisplay(baseNote, true, ContentScale.FillWidth, accountViewModel) + FileMetadataDisplay(baseNote, true, ContentScale.FillWidth, accountViewModel) } // Covers every NIP-71 video kind (21, 22, 34235, 34236) via the shared interface, @@ -1880,7 +1880,7 @@ private fun RenderNoteRow( } is BookmarkSetEvent -> { - RenderLabeledBookmarkList(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) + RenderBookmarkSet(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) } is ArticleCurationSetEvent -> { @@ -1924,7 +1924,7 @@ private fun RenderNoteRow( } is InterestListEvent -> { - RenderHashtagList(baseNote, backgroundColor, accountViewModel, nav) + RenderInterestList(baseNote, backgroundColor, accountViewModel, nav) } is InterestSetEvent -> { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index a6999b790b..27c3f60fd7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -116,8 +116,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.report.ReportNoteDialog import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import kotlinx.coroutines.launch import org.jetbrains.compose.resources.StringResource diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt index 5faaa6a2e2..415645df86 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt @@ -358,7 +358,7 @@ private fun EmojiSelector( onClick: ((EmojiUrlTag) -> Unit)? = null, ) { LoadAddressableNote( - accountViewModel.account.emoji.getEmojiPackSelectionAddress(), + accountViewModel.account.emoji.getEmojiListAddress(), ) { emptyNote -> emptyNote?.let { usersEmojiList -> val collections by observeNoteEventAndMapNotNull(usersEmojiList, accountViewModel) { event: EmojiListEvent -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index 753d91f79f..a5af97da5f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -52,7 +52,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.ScoreTag import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsScore +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAssertionsScore import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -565,7 +565,7 @@ fun ObserveAndRenderUserCards( modifier: Modifier = Modifier, accountViewModel: AccountViewModel, ) { - val score by observeUserContactCardsScore(user, accountViewModel) + val score by observeUserAssertionsScore(user, accountViewModel) score?.let { ScoreTag(it, size, modifier) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/AudienceSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/AudienceSheet.kt index bb78a2b1b1..ea8a60aca9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/AudienceSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/AudienceSheet.kt @@ -67,7 +67,7 @@ import com.vitorpamplona.amethyst.commons.model.composer.AudienceList import com.vitorpamplona.amethyst.commons.model.composer.AudienceListKind import com.vitorpamplona.amethyst.commons.model.composer.AudienceMember import com.vitorpamplona.amethyst.commons.model.composer.AudienceSelection -import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleList +import com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets.PeopleList import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.audience_add_anyway import com.vitorpamplona.amethyst.commons.resources.audience_add_people @@ -545,9 +545,9 @@ private fun MemberBadge( */ @Composable fun rememberAudienceLists(accountViewModel: AccountViewModel): List<AudienceList> { - val peopleLists by accountViewModel.account.peopleLists.uiListFlow + val peopleLists by accountViewModel.account.followSets.uiListFlow .collectAsStateWithLifecycle() - val followPacks by accountViewModel.account.followLists.uiListFlow + val followPacks by accountViewModel.account.starterPacks.uiListFlow .collectAsStateWithLifecycle() return remember(peopleLists, followPacks) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt index dabc6e7890..fd09217ed1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt @@ -281,7 +281,7 @@ fun observeBookmarksFollowsAndAccount( accountViewModel.account.bookmarkState.bookmarks, accountViewModel.account.pinState.pinnedEventIdSet, accountViewModel.showSensitiveContent(), - accountViewModel.account.emoji.getEmojiPackSelectionFlow(), + accountViewModel.account.emoji.getEmojiListFlow(), ) { follows, bookmarks, pinnedIds, showSensitiveContent, emojiSelectionState -> val isEmojiPackInMyList = if (noteIdForEmoji != null) { @@ -312,7 +312,7 @@ fun observeBookmarksFollowsAndAccount( isEmojiPackInMyList = noteIdForEmoji?.let { accountViewModel.account.emoji - .getEmojiPackSelection() + .getEmojiList() ?.isTaggedAddressableNote(it) == true } ?: false, ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ActivityCardPreviews.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ActivityCardPreviews.kt index d98200b394..d63b6a8880 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ActivityCardPreviews.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ActivityCardPreviews.kt @@ -103,10 +103,10 @@ private object ActivityCardPreviewData { } } - fun lnZapNote(): Note { + fun zapReceiptNote(): Note { val id = "d".repeat(64) // Receipts are signed by the lightning provider; the preview skips the - // embedded request and feeds the decrypted card to RenderLnZapCard directly. + // embedded request and feeds the decrypted card to RenderZapReceiptCard directly. return Note(id).apply { event = null author = null @@ -171,12 +171,12 @@ fun EmojiReactionActivityCardPreview() { @Preview @Composable -fun LnZapActivityCardPreview() { +fun ZapReceiptActivityCardPreview() { val accountViewModel = mockAccountViewModel() ThemeComparisonColumn { - RenderLnZapCard( - note = ActivityCardPreviewData.lnZapNote(), + RenderZapReceiptCard( + note = ActivityCardPreviewData.zapReceiptNote(), card = ZapAmountCommentNotification( user = LocalCache.getOrCreateUser(ActivityCardPreviewData.senderHex), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt index ebdf0422df..d65958b79d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt @@ -181,7 +181,7 @@ private fun EmojiListOptions( emojiPackNote: Note, ) { LoadAddressableNote( - accountViewModel.account.emoji.getEmojiPackSelectionAddress(), + accountViewModel.account.emoji.getEmojiListAddress(), ) { it?.let { usersEmojiList -> val hasAddedThis by observeNoteAndMap(usersEmojiList, accountViewModel) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt index bda74a2880..29017e1a15 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt @@ -38,7 +38,7 @@ import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent @Composable -fun FileHeaderDisplay( +fun FileMetadataDisplay( note: Note, roundedCorner: Boolean, contentScale: ContentScale, @@ -53,7 +53,7 @@ fun FileHeaderDisplay( // which viewer happens to render it, so an NSFW-tagged archive stays behind the same gate. SensitivityWarning(note = note, accountViewModel = accountViewModel) { if (content == null) { - FileHeaderAttachmentCard(event, fullUrl, mimeType) + FileMetadataAttachmentCard(event, fullUrl, mimeType) } else { ZoomableContentView( content = content, @@ -70,7 +70,7 @@ fun FileHeaderDisplay( * * Kind 1063 is a *generic* file container — its `m` tag can name any type, so unlike a NIP-71 * video event the kind itself asserts nothing about how to render the payload. A null here means - * the file belongs in [FileHeaderAttachmentCard] rather than being pushed into the video player. + * the file belongs in [FileMetadataAttachmentCard] rather than being pushed into the video player. */ internal fun FileMetadataEvent.toMediaContent( note: Note, @@ -128,7 +128,7 @@ internal fun FileMetadataEvent.toMediaContent( /** The link card a kind-1063 header falls back to when [toMediaContent] returns null. */ @Composable -internal fun FileHeaderAttachmentCard( +internal fun FileMetadataAttachmentCard( event: FileMetadataEvent, url: String, mimeType: String?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt index 2bee2e52d0..8e2aa280a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt @@ -38,7 +38,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent @Composable -fun RenderNIP90ContentDiscoveryResponse( +fun RenderDvmContentDiscoveryResponse( note: Note, makeItShort: Boolean, canPreview: Boolean, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt index 6b6bfb0303..19c27a2c83 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip90Dvms.status.DvmStatusEvent @Composable -fun RenderNIP90Status( +fun RenderDvmStatus( note: Note, accountViewModel: AccountViewModel, nav: INav, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt index 64b4fa7b39..1f5a19840b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt @@ -56,7 +56,7 @@ import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.getGradient import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds -import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ProfileCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ProfileCard.kt index 344fc539a2..c12f7659cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ProfileCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ProfileCard.kt @@ -76,7 +76,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.bitcoinColor import com.vitorpamplona.amethyst.commons.ui.theme.innerPostModifier import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsFollowerCount +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAssertionsFollowerCount import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowing import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor @@ -288,7 +288,7 @@ private fun ProfileCardChips( // A self-follow in your own kind:3 is common; "Follows you" on your own card is not a fact. val followsMe by observeUserIsFollowing(author, accountViewModel.account.userProfile(), accountViewModel) val followsYou = followsMe && !accountViewModel.isLoggedUser(author) - val followerCount by observeUserContactCardsFollowerCount(author, accountViewModel) + val followerCount by observeUserAssertionsFollowerCount(author, accountViewModel) // "--" is the placeholder the contact-card observer emits while no trusted // assertion for this user has arrived. Nothing to brag about yet. val followers = followerCount.takeIf { it != "--" } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt index ec1f485c88..3a5e7e4ed6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt @@ -241,14 +241,14 @@ fun DisplayTrustedRelayList( } @Composable -fun DisplayRelayFeedsList( +fun DisplayFavoriteRelayList( baseNote: Note, backgroundColor: MutableState<Color>, accountViewModel: AccountViewModel, nav: INav, ) { - val relays by accountViewModel.account.relayFeedsListDecryptionCache.observeDecryptedRelayList(baseNote).collectAsStateWithLifecycle( - accountViewModel.account.relayFeedsListDecryptionCache.fastStartValueForRelayList(baseNote), + val relays by accountViewModel.account.favoriteRelayListDecryptionCache.observeDecryptedRelayList(baseNote).collectAsStateWithLifecycle( + accountViewModel.account.favoriteRelayListDecryptionCache.fastStartValueForRelayList(baseNote), ) DisplayRelaySet( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/StarterPack.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/StarterPack.kt index ac06818f09..88ca1ebd7e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/StarterPack.kt @@ -66,13 +66,13 @@ import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.FollowSetCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @OptIn(ExperimentalLayoutApi::class) @Composable -fun DisplayFollowList( +fun DisplayStarterPack( baseNote: Note, makeItShort: Boolean, accountViewModel: AccountViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt index be61fdda0a..1412044f68 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt @@ -60,11 +60,11 @@ fun RenderWikiContent( ) { val noteEvent = note.event as? WikiArticleEvent ?: return - WikiNoteHeader(noteEvent, note, accountViewModel, nav) + WikiArticleHeader(noteEvent, note, accountViewModel, nav) } @Composable -private fun WikiNoteHeader( +private fun WikiArticleHeader( noteEvent: WikiArticleEvent, note: Note, accountViewModel: AccountViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt index bc6d1bb5f8..26815bdee9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ZapEvent.kt @@ -97,7 +97,7 @@ fun RenderZappedPost( } @Composable -fun RenderLnZap( +fun RenderZapReceipt( note: Note, quotesLeft: Int, backgroundColor: MutableState<Color>, @@ -108,7 +108,7 @@ fun RenderLnZap( val card by parseAuthorCommentAndAmount(note, accountViewModel) - RenderLnZapCard( + RenderZapReceiptCard( note = note, card = card, recipientKey = zapEvent.zappedAuthor().firstOrNull(), @@ -120,7 +120,7 @@ fun RenderLnZap( } @Composable -fun RenderLnZapCard( +fun RenderZapReceiptCard( note: Note, card: ZapAmountCommentNotification, recipientKey: String?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/BookmarkLists.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/BookmarkLists.kt index 2a8a1ce57b..24ccbbe3c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/BookmarkLists.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/BookmarkLists.kt @@ -38,7 +38,7 @@ import com.vitorpamplona.quartz.nip51Lists.articleCurationSet.ArticleCurationSet import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.BookmarkSetEvent import com.vitorpamplona.quartz.nip51Lists.pictureCurationSet.PictureCurationSetEvent /** NIP-51 kind 10003: the user's bookmarks — notes, articles, hashtags and links. */ @@ -101,7 +101,7 @@ fun RenderOldBookmarkList( /** NIP-51 kind 30003: "user-defined bookmarks categories, for when bookmarks must be in labeled separate groups". */ @Composable -fun RenderLabeledBookmarkList( +fun RenderBookmarkSet( baseNote: Note, quotesLeft: Int, backgroundColor: MutableState<Color>, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/TagLists.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/TagLists.kt index b2186cbd21..993591f7cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/TagLists.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/lists/TagLists.kt @@ -36,12 +36,12 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent /** NIP-51 kind 10015: the hashtags this user follows. */ @Composable -fun RenderHashtagList( +fun RenderInterestList( baseNote: Note, backgroundColor: MutableState<Color>, accountViewModel: AccountViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt index b8f38aab80..4b6d470a1c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt @@ -303,8 +303,8 @@ class AccountSessionManager( accountSettings.backupKeyPackageRelayList?.let { client.publish(it, toPost) } accountSettings.backupSearchRelayList?.let { client.publish(it, toPost) } accountSettings.backupIndexRelayList?.let { client.publish(it, toPost) } - accountSettings.backupRelayFeedsList?.let { client.publish(it, toPost) } - accountSettings.backupChannelList?.let { client.publish(it, toPost) } + accountSettings.backupFavoriteRelayList?.let { client.publish(it, toPost) } + accountSettings.backupPublicChatList?.let { client.publish(it, toPost) } accountSettings.backupCashuWallet?.let { client.publish(it, toPost) } accountSettings.backupNutzapInfo?.let { client.publish(it, toPost) } } @@ -329,8 +329,8 @@ class AccountSessionManager( backupKeyPackageRelayList = bootstrap.keyPackageRelayList, backupSearchRelayList = bootstrap.searchRelayList, backupIndexRelayList = bootstrap.indexerRelayList, - backupChannelList = bootstrap.channelList, - backupRelayFeedsList = bootstrap.relayFeedsList, + backupPublicChatList = bootstrap.publicChatList, + backupFavoriteRelayList = bootstrap.favoriteRelayList, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt index 8d0618d42d..04e304dcee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt @@ -43,9 +43,9 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.utils.Log @@ -162,14 +162,14 @@ class TopNavFilterState( val livePeopleListsFlow: Flow<List<FeedDefinition>> = combine( - account.peopleLists.peopleListNotes, - account.followLists.followListNotes, + account.followSets.peopleListNotes, + account.starterPacks.starterPackNotes, ::mergePeopleLists, ).onStart { emit( mergePeopleLists( - account.peopleLists.peopleListNotes.value, - account.followLists.followListNotes.value, + account.followSets.peopleListNotes.value, + account.starterPacks.starterPackNotes.value, ), ) } @@ -247,10 +247,10 @@ class TopNavFilterState( @OptIn(ExperimentalCoroutinesApi::class) val liveInterestFlows: Flow<List<FeedDefinition>> = combine( - account.hashtagList.flow, + account.interestList.flow, account.geohashList.flow, account.communityList.flowNotes, - account.relayFeedsList.flow, + account.favoriteRelayList.flow, combine( account.favoriteAlgoFeedsList.flowNotes, account.interestSets.listFeedFlow, @@ -261,10 +261,10 @@ class TopNavFilterState( }.onStart { emit( mergeInterests( - account.hashtagList.flow.value, + account.interestList.flow.value, account.geohashList.flow.value, account.communityList.flowNotes.value, - account.relayFeedsList.flow.value, + account.favoriteRelayList.flow.value, account.favoriteAlgoFeedsList.flowNotes.value, account.interestSets.listFeedFlow.value, ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 6cd968367e..064b4af887 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -202,7 +202,7 @@ import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark -import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent import com.vitorpamplona.quartz.nip56Reports.ReportType import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent @@ -2144,11 +2144,11 @@ class AccountViewModel( fun hide(user: User) = launchSigner { account.hideUser(user.pubkeyHex) } - fun updateContactCardPetName( + fun updateUserAssertionPetName( user: User, petName: String?, summary: String?, - ) = launchSigner { account.updateContactCardPetName(user.pubkeyHex, petName, summary) } + ) = launchSigner { account.updateUserAssertionPetName(user.pubkeyHex, petName, summary) } fun hide(word: String) = launchSigner { account.hideWord(word) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index 794e1b3027..e09c5e6e4a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -68,10 +68,10 @@ class EventProcessor( private val draftHandler = DraftEventHandler(account, cache) private val giftWrapHandler = GiftWrapEventHandler(account, cache, this) - private val sealHandler = SealedRumorEventHandler(account, cache, this) + private val sealHandler = SealEventHandler(account, cache, this) - private val zapRequest = LnZapRequestEventHandler(account.privateZapsDecryptionCache) - private val zapEvent = LnZapEventHandler(account.privateZapsDecryptionCache) + private val zapRequest = ZapRequestEventHandler(account.privateZapsDecryptionCache) + private val zapEvent = ZapReceiptEventHandler(account.privateZapsDecryptionCache) private val groupEventHandler = GroupEventHandler(account, cache) @@ -389,7 +389,7 @@ class GiftWrapEventHandler( /** * Shared Marmot Welcome handler used by both [GiftWrapEventHandler] * (in case a Welcome arrives directly inside a kind:1059 with no Seal - * layer) and [SealedRumorEventHandler] (the actual production path — + * layer) and [SealEventHandler] (the actual production path — * Welcomes are wrapped GiftWrap → Seal → Welcome per * [com.vitorpamplona.quartz.marmot.mip02Welcome.WelcomeGiftWrap]). */ @@ -453,7 +453,7 @@ private suspend fun processMarmotWelcomeFlow( } } -class SealedRumorEventHandler( +class SealEventHandler( private val account: Account, private val cache: LocalCache, private val eventProcessor: EventProcessor, @@ -465,14 +465,14 @@ class SealedRumorEventHandler( ) { val rumorId = event.innerEventId if (rumorId == null) { - processNewSealedRumor(event, eventNote, publicNote) + processNewSeal(event, eventNote, publicNote) } else { // Replayed seal: re-link the rumor to its delivering envelope so // broadcast can republish the wrap after a cache rebuild. // publicNote is the outermost event of this unwrap chain — the // kind-1059 wrap normally, the seal itself when it arrived bare. publicNote.event?.let { envelope -> cache.getOrCreateNote(rumorId).recordRumorHost(envelope) } - processExistingSealedRumor(rumorId, publicNote) + processExistingSeal(rumorId, publicNote) } } @@ -488,7 +488,7 @@ class SealedRumorEventHandler( } } - private suspend fun processNewSealedRumor( + private suspend fun processNewSeal( event: SealEvent, eventNote: Note, publicNote: Note, @@ -524,7 +524,7 @@ class SealedRumorEventHandler( eventNote.flowSet?.metadata?.invalidateData() } - private suspend fun processExistingSealedRumor( + private suspend fun processExistingSeal( rumorId: String, publicNote: Note, ) { @@ -536,7 +536,7 @@ class SealedRumorEventHandler( } } -class LnZapRequestEventHandler( +class ZapRequestEventHandler( val decryptionCache: PrivateZapCache, ) : EventHandler<ZapRequestEvent> { override suspend fun add( @@ -559,7 +559,7 @@ class LnZapRequestEventHandler( } } -class LnZapEventHandler( +class ZapReceiptEventHandler( val decryptionCache: PrivateZapCache, ) : EventHandler<ZapReceiptEvent> { override suspend fun delete( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt index bb977a4184..90802e1b17 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt @@ -65,10 +65,10 @@ import com.vitorpamplona.quartz.nip01Core.diff.ContentChange import com.vitorpamplona.quartz.nip01Core.diff.EventDiff import com.vitorpamplona.quartz.nip01Core.metadata.MetadataDiff import com.vitorpamplona.quartz.nip02FollowList.ContactListDiff -import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListDiff +import com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListDiff import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListDiff import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListDiff -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListDiff +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListDiff import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListDiff import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayListDiff import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListDiff @@ -130,12 +130,12 @@ private fun headlineOf( is MetadataDiff -> plural(R.plurals.backup_card_profile_lost_fields, counts.removed) is AdvertisedRelayListDiff -> plural(R.plurals.backup_card_nip65_lost, diff.relays.removed.size) is RelayListDiff -> relayListHeadline(conflict.eventType, diff.relays.removed.size) - is ChannelListDiff -> plural(R.plurals.backup_card_leave_chats, diff.channels.removed.size) + is PublicChatListDiff -> plural(R.plurals.backup_card_leave_chats, diff.channels.removed.size) is CommunityListDiff -> plural(R.plurals.backup_card_leave_communities, diff.communities.removed.size) is EphemeralChatListDiff -> plural(R.plurals.backup_card_leave_rooms, diff.rooms.removed.size) is SimpleGroupListDiff -> plural(R.plurals.backup_card_leave_groups, diff.groups.removed.size) is FavoriteAlgoFeedsListDiff -> plural(R.plurals.backup_card_feeds_removed, diff.feeds.removed.size) - is HashtagListDiff -> plural(R.plurals.backup_card_hashtags_unfollowed, diff.hashtags.removed.size) + is InterestListDiff -> plural(R.plurals.backup_card_hashtags_unfollowed, diff.hashtags.removed.size) is GeohashListDiff -> plural(R.plurals.backup_card_places_unfollowed, diff.geohashes.removed.size) is TrustProviderListDiff -> plural(R.plurals.backup_card_trust_services_changed, diff.providers.removed.size + diff.providers.changed.size) is NutzapInfoDiff -> @@ -162,7 +162,7 @@ private fun headlineOf( when { diff is ContactListDiff -> R.plurals.backup_card_and_followed diff is MuteListDiff -> R.plurals.backup_card_and_muted - diff is ChannelListDiff || diff is CommunityListDiff || diff is EphemeralChatListDiff || diff is SimpleGroupListDiff -> R.plurals.backup_card_and_joined + diff is PublicChatListDiff || diff is CommunityListDiff || diff is EphemeralChatListDiff || diff is SimpleGroupListDiff -> R.plurals.backup_card_and_joined conflict.eventType == BackupEventType.BLOCKED_RELAYS -> R.plurals.backup_card_and_blocked else -> R.plurals.backup_card_and_added } @@ -182,12 +182,12 @@ private fun addedToHeadlinedList( is MuteListDiff -> diff.publicMutes.added.size is AdvertisedRelayListDiff -> diff.relays.added.size is RelayListDiff -> if (relayListHasHeadline(type)) diff.relays.added.size else 0 - is ChannelListDiff -> diff.channels.added.size + is PublicChatListDiff -> diff.channels.added.size is CommunityListDiff -> diff.communities.added.size is EphemeralChatListDiff -> diff.rooms.added.size is SimpleGroupListDiff -> diff.groups.added.size is FavoriteAlgoFeedsListDiff -> diff.feeds.added.size - is HashtagListDiff -> diff.hashtags.added.size + is InterestListDiff -> diff.hashtags.added.size is GeohashListDiff -> diff.geohashes.added.size is NutzapInfoDiff -> if (diff.p2pkPubkey == null) diff.mints.added.size else 0 is PaymentTargetsDiff -> diff.targets.added.size diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt index 6587568294..5c67889add 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt @@ -93,7 +93,7 @@ import com.vitorpamplona.quartz.nip01Core.diff.ContentChange import com.vitorpamplona.quartz.nip01Core.diff.ListDiff import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent -import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListDiff +import com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListDiff import com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListEvent import com.vitorpamplona.quartz.nip28PublicChat.list.channels import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent @@ -102,8 +102,8 @@ import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFee import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListDiff import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListDiff -import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListDiff +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent @@ -133,11 +133,11 @@ internal fun LazyListScope.listDiffItems( ): Boolean { when (val diff = conflict.diff) { is RelayListDiff -> relayListItems(diff, conflict, nav) - is ChannelListDiff -> publicChatItems(diff, conflict, accountViewModel, nav) + is PublicChatListDiff -> publicChatItems(diff, conflict, accountViewModel, nav) is CommunityListDiff -> communityItems(diff, conflict, accountViewModel, nav) is EphemeralChatListDiff -> ephemeralRoomItems(diff, conflict, nav) is FavoriteAlgoFeedsListDiff -> favoriteFeedItems(diff, conflict, accountViewModel, nav) - is HashtagListDiff -> topicItems(diff.hashtags, (conflict.saved as? InterestListEvent)?.publicHashtags().orEmpty(), diff.privateItems, isPlace = false, nav) + is InterestListDiff -> topicItems(diff.hashtags, (conflict.saved as? InterestListEvent)?.publicHashtags().orEmpty(), diff.privateItems, isPlace = false, nav) is GeohashListDiff -> topicItems(diff.geohashes, (conflict.saved as? GeohashListEvent)?.publicGeohashes().orEmpty(), diff.privateItems, isPlace = true, nav) is TrustProviderListDiff -> trustProviderItems(diff, conflict, accountViewModel, nav) is PaymentTargetsDiff -> paymentTargetItems(diff, conflict) @@ -446,7 +446,7 @@ private fun SpaceTile( } private fun LazyListScope.publicChatItems( - diff: ChannelListDiff, + diff: PublicChatListDiff, conflict: ReplaceableBackupConflict, accountViewModel: AccountViewModel, nav: INav, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt index 58195a5d31..9b390254f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt @@ -41,10 +41,10 @@ import com.vitorpamplona.quartz.nip01Core.metadata.Birthday import com.vitorpamplona.quartz.nip01Core.metadata.MetadataDiff import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip02FollowList.ContactListDiff -import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListDiff +import com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListDiff import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListDiff import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListDiff -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListDiff +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListDiff import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListDiff import com.vitorpamplona.quartz.nip51Lists.muteList.tags.EventTag import com.vitorpamplona.quartz.nip51Lists.muteList.tags.HashtagTag @@ -104,9 +104,9 @@ fun countsOf(diff: EventDiff): ConflictCounts = is MuteListDiff -> diff.publicMutes.asCounts() + diff.privateItems.asCounts() is AdvertisedRelayListDiff -> diff.relays.asCounts() is RelayListDiff -> diff.relays.asCounts() + diff.privateRelays.asCounts() - is ChannelListDiff -> diff.channels.asCounts() + diff.privateItems.asCounts() + is PublicChatListDiff -> diff.channels.asCounts() + diff.privateItems.asCounts() is CommunityListDiff -> diff.communities.asCounts() + diff.privateItems.asCounts() - is HashtagListDiff -> diff.hashtags.asCounts() + diff.privateItems.asCounts() + is InterestListDiff -> diff.hashtags.asCounts() + diff.privateItems.asCounts() is GeohashListDiff -> diff.geohashes.asCounts() + diff.privateItems.asCounts() is FavoriteAlgoFeedsListDiff -> diff.feeds.asCounts() + diff.privateItems.asCounts() is EphemeralChatListDiff -> diff.rooms.asCounts() + diff.privateItems.asCounts() @@ -330,7 +330,7 @@ fun presentationOf( ) to ContentChange.NONE } is RelayListDiff -> listOf(listGroup(R.string.backup_entry_relay, diff.relays, { ReviewItem.Relay(it) })) to diff.privateRelays - is ChannelListDiff -> + is PublicChatListDiff -> listOf( listGroup( R.string.backup_entry_public_chat, @@ -348,7 +348,7 @@ fun presentationOf( { ReviewItem.Addressable(it.after.address, arrow(it.before.relayHint?.url, it.after.relayHint?.url)) }, ), ) to diff.privateItems - is HashtagListDiff -> listOf(listGroup(R.string.backup_entry_hashtag, diff.hashtags, { ReviewItem.Text("#$it") })) to diff.privateItems + is InterestListDiff -> listOf(listGroup(R.string.backup_entry_hashtag, diff.hashtags, { ReviewItem.Text("#$it") })) to diff.privateItems is GeohashListDiff -> listOf(listGroup(R.string.backup_entry_location, diff.geohashes, { ReviewItem.Text(it) })) to diff.privateItems is FavoriteAlgoFeedsListDiff -> listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt index 8b5d7b0150..805e43fabc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt @@ -86,10 +86,10 @@ import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListDif import com.vitorpamplona.quartz.nip01Core.diff.ContentChange import com.vitorpamplona.quartz.nip01Core.metadata.MetadataDiff import com.vitorpamplona.quartz.nip02FollowList.ContactListDiff -import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListDiff +import com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListDiff import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListDiff import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListDiff -import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListDiff +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListDiff import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListDiff import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayListDiff import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListDiff @@ -373,11 +373,11 @@ private fun actionLabels(conflict: ReplaceableBackupConflict): Pair<String, Stri stringRes(R.string.backup_conflict_keep_new) to stringRes(R.string.backup_action_restore_relays) } is AdvertisedRelayListDiff -> stringRes(R.string.backup_conflict_keep_new) to stringRes(R.string.backup_action_restore_relays) - is ChannelListDiff -> rejoinLabels(diff.channels.removed.size) + is PublicChatListDiff -> rejoinLabels(diff.channels.removed.size) is CommunityListDiff -> rejoinLabels(diff.communities.removed.size) is EphemeralChatListDiff -> rejoinLabels(diff.rooms.removed.size) is FavoriteAlgoFeedsListDiff -> stringRes(R.string.backup_conflict_keep_new) to stringRes(R.string.backup_action_restore_feeds) - is HashtagListDiff -> stringRes(R.string.backup_conflict_keep_new) to stringRes(R.string.backup_action_restore_topics) + is InterestListDiff -> stringRes(R.string.backup_conflict_keep_new) to stringRes(R.string.backup_action_restore_topics) // Geohashes are places, not topics: the screen counts "places gone" under "Followed // locations", so the button has to agree with the page it sits on. is GeohashListDiff -> stringRes(R.string.backup_conflict_keep_new) to stringRes(R.string.backup_action_restore_places) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupScreen.kt index 15c2317fcd..a951f6deb1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupScreen.kt @@ -101,7 +101,7 @@ fun BookmarkGroupScreen( bookmarkType, broadcastBookmarkGroup = { accountViewModel.launchSigner { - val groupNote = accountViewModel.account.labeledBookmarkLists.getLabeledBookmarkListNote(bookmarkIdentifier) + val groupNote = accountViewModel.account.bookmarkSets.getBookmarkSetNote(bookmarkIdentifier) groupNote?.let { accountViewModel.broadcast(it) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupViewModel.kt index 9cba1b5ad4..c23afd9fb3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/display/BookmarkGroupViewModel.kt @@ -40,8 +40,8 @@ class BookmarkGroupViewModel( val bookmarkGroupIdentifier: String, ) : ViewModel() { val selectedBookmarkGroupFlow = - account.labeledBookmarkLists - .getLabeledBookmarkListFlow(bookmarkGroupIdentifier) + account.bookmarkSets + .getBookmarkSetFlow(bookmarkGroupIdentifier) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(2500), null) fun publicPosts() = @@ -69,7 +69,7 @@ class BookmarkGroupViewModel( .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) suspend fun deleteBookmarkGroup(groupIdentifier: String) { - account.labeledBookmarkLists.deleteBookmarkList(groupIdentifier, account) + account.bookmarkSets.deleteBookmarkList(groupIdentifier, account) } suspend fun addBookmarkToGroup( @@ -77,7 +77,7 @@ class BookmarkGroupViewModel( bookmark: BookmarkIdTag, isPrivate: Boolean, ) { - account.labeledBookmarkLists.addBookmarkToList( + account.bookmarkSets.addBookmarkToList( bookmark, groupIdentifier, isPrivate, @@ -108,7 +108,7 @@ class BookmarkGroupViewModel( bookmark: BookmarkIdTag, isCurrentlyPrivate: Boolean, ) { - account.labeledBookmarkLists.moveBookmarkInList( + account.bookmarkSets.moveBookmarkInList( bookmark, groupIdentifier, isCurrentlyPrivate, @@ -147,7 +147,7 @@ class BookmarkGroupViewModel( bookmark: BookmarkIdTag, isPrivate: Boolean, ) { - account.labeledBookmarkLists.removeBookmarkFromList( + account.bookmarkSets.removeBookmarkFromList( bookmark, groupIdentifier, isPrivate, @@ -160,7 +160,7 @@ class BookmarkGroupViewModel( deletedEventIds: Set<String>, deletedAddresses: Set<Address>, ) { - account.labeledBookmarkLists.removeDeletedBookmarksFromList( + account.bookmarkSets.removeDeletedBookmarksFromList( groupIdentifier, deletedEventIds, deletedAddresses, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsScreen.kt index 18556e4e82..727d168bf8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/ListOfBookmarkGroupsScreen.kt @@ -40,7 +40,7 @@ import com.vitorpamplona.amethyst.commons.model.nip51Lists.BookmarkType import com.vitorpamplona.amethyst.commons.model.nip51Lists.GitRepositoryListState import com.vitorpamplona.amethyst.commons.model.nip51Lists.OldBookmarkListState import com.vitorpamplona.amethyst.commons.model.nip51Lists.PinListState -import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList +import com.vitorpamplona.amethyst.commons.model.nip51Lists.bookmarkSets.BookmarkSet import com.vitorpamplona.amethyst.commons.nip51Lists.ui.ListOfBookmarkGroupsFeedView import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.bookmark_lists @@ -64,7 +64,7 @@ fun ListOfBookmarkGroupsScreen( oldBookmarks = accountViewModel.account.oldBookmarkState, pinnedNotes = accountViewModel.account.pinState, repositories = accountViewModel.account.gitRepositoryListState, - listSource = accountViewModel.account.labeledBookmarkLists.listFeedFlow, + listSource = accountViewModel.account.bookmarkSets.listFeedFlow, openDefaultBookmarks = { nav.nav(Route.Bookmarks) }, openOldBookmarks = { nav.nav(Route.OldBookmarks) }, openPinnedNotes = { nav.nav(Route.PinnedNotes) }, @@ -82,7 +82,7 @@ fun ListOfBookmarkGroupsScreen( }, cloneBookmarkGroup = { bookmarkGroup, customName, customDesc -> accountViewModel.launchSigner { - accountViewModel.account.labeledBookmarkLists.cloneBookmarkList( + accountViewModel.account.bookmarkSets.cloneBookmarkList( currentBookmarkList = bookmarkGroup, customCloneName = customName, customCloneDescription = customDesc, @@ -92,7 +92,7 @@ fun ListOfBookmarkGroupsScreen( }, deleteBookmarkGroup = { bookmarkGroup -> accountViewModel.launchSigner { - accountViewModel.account.labeledBookmarkLists.deleteBookmarkList( + accountViewModel.account.bookmarkSets.deleteBookmarkList( bookmarkListIdentifier = bookmarkGroup.identifier, account = accountViewModel.account, ) @@ -109,7 +109,7 @@ fun ListOfBookmarkGroupsFeed( oldBookmarks: OldBookmarkListState, pinnedNotes: PinListState, repositories: GitRepositoryListState, - listSource: StateFlow<List<LabeledBookmarkList>>, + listSource: StateFlow<List<BookmarkSet>>, openDefaultBookmarks: () -> Unit, openOldBookmarks: () -> Unit, openPinnedNotes: () -> Unit, @@ -117,10 +117,10 @@ fun ListOfBookmarkGroupsFeed( openPodcasts: () -> Unit, addBookmarkGroup: () -> Unit, openBookmarkGroup: (identifier: String, bookmarkType: BookmarkType) -> Unit, - renameBookmarkGroup: (bookmarkGroup: LabeledBookmarkList) -> Unit, - changeBookmarkGroupDescription: (bookmarkGroup: LabeledBookmarkList) -> Unit, - cloneBookmarkGroup: (bookmarkGroup: LabeledBookmarkList, customName: String?, customDesc: String?) -> Unit, - deleteBookmarkGroup: (bookmarkGroup: LabeledBookmarkList) -> Unit, + renameBookmarkGroup: (bookmarkGroup: BookmarkSet) -> Unit, + changeBookmarkGroupDescription: (bookmarkGroup: BookmarkSet) -> Unit, + cloneBookmarkGroup: (bookmarkGroup: BookmarkSet, customName: String?, customDesc: String?) -> Unit, + deleteBookmarkGroup: (bookmarkGroup: BookmarkSet) -> Unit, accountViewModel: AccountViewModel, nav: INav, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt index d8e41c8398..1f2f4bc31a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/list/metadata/BookmarkGroupMetadataViewModel.kt @@ -31,7 +31,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType -import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList +import com.vitorpamplona.amethyst.commons.model.nip51Lists.bookmarkSets.BookmarkSet import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details @@ -59,7 +59,7 @@ class BookmarkGroupMetadataViewModel : ViewModel() { private lateinit var accountViewModel: AccountViewModel private lateinit var account: Account - var bookmarkGroup by mutableStateOf<LabeledBookmarkList?>(null) + var bookmarkGroup by mutableStateOf<BookmarkSet?>(null) val isNewList by derivedStateOf { bookmarkGroup == null } val name = mutableStateOf(TextFieldValue()) @@ -83,7 +83,7 @@ class BookmarkGroupMetadataViewModel : ViewModel() { } fun load(dTag: String) { - bookmarkGroup = account.labeledBookmarkLists.getBookmarkList(dTag) + bookmarkGroup = account.bookmarkSets.getBookmarkList(dTag) name.value = TextFieldValue(bookmarkGroup?.title ?: "") picture.value = TextFieldValue(bookmarkGroup?.image ?: "") description.value = TextFieldValue(bookmarkGroup?.description ?: "") @@ -95,14 +95,14 @@ class BookmarkGroupMetadataViewModel : ViewModel() { accountViewModel.launchSigner { val bookmarkGroup = bookmarkGroup if (bookmarkGroup == null) { - accountViewModel.account.labeledBookmarkLists.addLabeledBookmarkList( + accountViewModel.account.bookmarkSets.addBookmarkSet( listName = name.value.text, listDescription = description.value.text, listImage = picture.value.text, account = accountViewModel.account, ) } else { - accountViewModel.account.labeledBookmarkLists.updateMetadata( + accountViewModel.account.bookmarkSets.updateMetadata( listName = name.value.text, listDescription = description.value.text, listImage = picture.value.text, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt index d1e3f9749c..c562d73658 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/ArticleBookmarkListManagementScreen.kt @@ -37,7 +37,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.nip51Lists.BookmarkType -import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList +import com.vitorpamplona.amethyst.commons.model.nip51Lists.bookmarkSets.BookmarkSet import com.vitorpamplona.amethyst.commons.nip51Lists.ui.BookmarkGroupManagementItem import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.article_bookmark_management_title @@ -109,7 +109,7 @@ private fun ListManagementViewBody( accountViewModel: AccountViewModel, nav: INav, ) { - val bookmarkGroups by accountViewModel.account.labeledBookmarkLists.listFeedFlow + val bookmarkGroups by accountViewModel.account.bookmarkSets.listFeedFlow .collectAsStateWithLifecycle() val defaultBookmarks by accountViewModel.account.bookmarkState.bookmarks @@ -146,7 +146,7 @@ private fun ListManagementViewBody( ) } - itemsIndexed(items = bookmarkGroups, key = { _: Int, item: LabeledBookmarkList -> item.identifier }) { _, bookmarkList -> + itemsIndexed(items = bookmarkGroups, key = { _: Int, item: BookmarkSet -> item.identifier }) { _, bookmarkList -> val maybePublicBookmark = bookmarkList.publicArticleBookmarks.firstOrNull { it.address == note.address } val maybePrivateBookmark = bookmarkList.privateArticleBookmarks.firstOrNull { it.address == note.address } BookmarkGroupManagementItem( @@ -159,7 +159,7 @@ private fun ListManagementViewBody( onClick = { nav.nav(Route.BookmarkGroupView(bookmarkList.identifier, BookmarkType.ArticleBookmark)) }, onAddBookmarkToGroup = { shouldBePrivate -> accountViewModel.launchSigner { - accountViewModel.account.labeledBookmarkLists.addBookmarkToList( + accountViewModel.account.bookmarkSets.addBookmarkToList( bookmark = AddressBookmark(address = note.address, relayHint = note.relayHintUrl()), bookmarkListIdentifier = bookmarkList.identifier, isBookmarkPrivate = shouldBePrivate, @@ -169,7 +169,7 @@ private fun ListManagementViewBody( }, onRemoveBookmarkFromGroup = { accountViewModel.launchSigner { - accountViewModel.account.labeledBookmarkLists.removeBookmarkFromList( + accountViewModel.account.bookmarkSets.removeBookmarkFromList( bookmark = AddressBookmark(address = note.address), bookmarkListIdentifier = bookmarkList.identifier, isBookmarkPrivate = maybePrivateBookmark != null, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt index 12e2a0860a..49564f47a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarkgroups/membershipManagement/PostBookmarkListManagementScreen.kt @@ -37,7 +37,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.nip51Lists.BookmarkType -import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList +import com.vitorpamplona.amethyst.commons.model.nip51Lists.bookmarkSets.BookmarkSet import com.vitorpamplona.amethyst.commons.nip51Lists.ui.BookmarkGroupManagementItem import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.bookmarks_title @@ -108,7 +108,7 @@ private fun ListManagementViewBody( accountViewModel: AccountViewModel, nav: INav, ) { - val bookmarkGroups by accountViewModel.account.labeledBookmarkLists.listFeedFlow + val bookmarkGroups by accountViewModel.account.bookmarkSets.listFeedFlow .collectAsStateWithLifecycle() val defaultBookmarks by accountViewModel.account.bookmarkState.bookmarks @@ -145,7 +145,7 @@ private fun ListManagementViewBody( ) } - itemsIndexed(items = bookmarkGroups, key = { _: Int, item: LabeledBookmarkList -> item.identifier }) { _, bookmarkList -> + itemsIndexed(items = bookmarkGroups, key = { _: Int, item: BookmarkSet -> item.identifier }) { _, bookmarkList -> val maybePublicBookmark = bookmarkList.publicPostBookmarks.firstOrNull { it.eventId == note.idHex } val maybePrivateBookmark = bookmarkList.privatePostBookmarks.firstOrNull { it.eventId == note.idHex } BookmarkGroupManagementItem( @@ -158,7 +158,7 @@ private fun ListManagementViewBody( onClick = { nav.nav(Route.BookmarkGroupView(bookmarkList.identifier, BookmarkType.PostBookmark)) }, onAddBookmarkToGroup = { shouldBePrivate -> accountViewModel.launchSigner { - accountViewModel.account.labeledBookmarkLists.addBookmarkToList( + accountViewModel.account.bookmarkSets.addBookmarkToList( bookmark = EventBookmark(eventId = note.idHex, relay = note.relayHintUrl(), author = note.author?.pubkeyHex), bookmarkListIdentifier = bookmarkList.identifier, isBookmarkPrivate = shouldBePrivate, @@ -168,7 +168,7 @@ private fun ListManagementViewBody( }, onRemoveBookmarkFromGroup = { accountViewModel.launchSigner { - accountViewModel.account.labeledBookmarkLists.removeBookmarkFromList( + accountViewModel.account.bookmarkSets.removeBookmarkFromList( bookmark = EventBookmark(eventId = note.idHex), bookmarkListIdentifier = bookmarkList.identifier, isBookmarkPrivate = maybePrivateBookmark != null, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupDiscoveryScreen.kt index afdf3562b4..93ea1bb03c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupDiscoveryScreen.kt @@ -250,7 +250,7 @@ private fun WatchAccountForRelayGroupDiscovery( .collectAsStateWithLifecycle() val joinedServers by accountViewModel.account.relayGroupList.liveRelayGroupServers .collectAsStateWithLifecycle() - val favoriteRelays by accountViewModel.account.relayFeedsList.flow + val favoriteRelays by accountViewModel.account.favoriteRelayList.flow .collectAsStateWithLifecycle() // Discovery only shows groups whose 39000 is signed by the host relay's own key (NIP-29's @@ -349,7 +349,7 @@ private fun RelayRailHeader( val info = loadRelayInfo(relay) val host = relay.displayUrl() val name = info.value.name?.takeIf { it.isNotBlank() } ?: host - val favoriteRelays by accountViewModel.account.relayFeedsList.flow + val favoriteRelays by accountViewModel.account.favoriteRelayList.flow .collectAsStateWithLifecycle() val isFavorite = relay in favoriteRelays diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMembersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMembersScreen.kt index 9d9b4b5e5c..d94934ee10 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMembersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupMembersScreen.kt @@ -408,7 +408,7 @@ private fun RelayGroupMemberRow( ) } DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) { - val declaredRoles = channel.supportedRoles + val declaredRoles = channel.groupRoles if (declaredRoles.isNotEmpty()) { // The relay declares its own role set (kind 39003) — offer exactly those // instead of the built-in admin/moderator pair. Roles are privilege grants, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsDiscoverySubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsDiscoverySubAssembler.kt index 8d0897ae01..23715d84f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsDiscoverySubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsDiscoverySubAssembler.kt @@ -100,7 +100,7 @@ class RelayGroupsDiscoverySubAssembler( account.relayGroupList.liveRelayGroupServers.value.forEach { server -> RelayUrlNormalizer.normalizeOrNull(server)?.let(::add) } - addAll(account.relayFeedsList.flow.value) + addAll(account.favoriteRelayList.flow.value) } override fun user(key: RelayGroupsDiscoveryQueryState) = key.account.userProfile() @@ -156,7 +156,7 @@ class RelayGroupsDiscoverySubAssembler( } }, key.account.scope.launch(Dispatchers.IO) { - key.account.relayFeedsList.flow.sample(500).collectLatest { + key.account.favoriteRelayList.flow.sample(500).collectLatest { invalidateFilters() } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt index b6eb04d860..f4e4212c37 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt @@ -45,7 +45,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.RenderCo import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.RenderClassifiedsThumb import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt index 5d28a436a8..f85d225362 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt @@ -88,7 +88,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.Discove import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.TabItem import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt index 2e09975f62..1e0fdcad4d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent open class DiscoverFollowSetsFeedFilter( val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt index d7399581f6..3e249d4a59 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt @@ -63,7 +63,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageBannerBackground import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt index ef283a4950..a0f559056c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt @@ -87,7 +87,7 @@ import com.vitorpamplona.amethyst.ui.screen.RenderFeedState import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.DVMCard -import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.dal.NIP90ContentDiscoveryFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.dal.DvmContentDiscoveryFeedViewModel import com.vitorpamplona.quartz.lightning.LnInvoiceUtil import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent @@ -263,22 +263,22 @@ fun PrepareViewContentDiscoveryModels( accountViewModel: AccountViewModel, nav: INav, ) { - val resultFeedViewModel: NIP90ContentDiscoveryFeedViewModel = + val resultFeedViewModel: DvmContentDiscoveryFeedViewModel = viewModel( key = "NostrNIP90ContentDiscoveryFeedViewModel${dvm.pubkeyHex}$dvmRequestId", - factory = NIP90ContentDiscoveryFeedViewModel.Factory(accountViewModel.account, dvmKey = dvm.pubkeyHex, requestId = dvmRequestId), + factory = DvmContentDiscoveryFeedViewModel.Factory(accountViewModel.account, dvmKey = dvm.pubkeyHex, requestId = dvmRequestId), ) LaunchedEffect(key1 = dvmRequestId, latestResponse.id) { resultFeedViewModel.invalidateData() } - RenderNostrNIP90ContentDiscoveryScreen(resultFeedViewModel, onRefresh, accountViewModel, nav) + RenderNostrDvmContentDiscoveryScreen(resultFeedViewModel, onRefresh, accountViewModel, nav) } @Composable -fun RenderNostrNIP90ContentDiscoveryScreen( - resultFeedViewModel: NIP90ContentDiscoveryFeedViewModel, +fun RenderNostrDvmContentDiscoveryScreen( + resultFeedViewModel: DvmContentDiscoveryFeedViewModel, onRefresh: () -> Unit, accountViewModel: AccountViewModel, nav: INav, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryFeedViewModel.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryFeedViewModel.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryFeedViewModel.kt index 5bf18acf92..6e16027d13 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryFeedViewModel.kt @@ -27,17 +27,17 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.screen.AndroidFeedViewModel @Stable -class NIP90ContentDiscoveryFeedViewModel( +class DvmContentDiscoveryFeedViewModel( val account: Account, dvmKey: String, requestId: String, -) : AndroidFeedViewModel(NIP90ContentDiscoveryResponseFilter(account, dvmKey, requestId)) { +) : AndroidFeedViewModel(DvmContentDiscoveryResponseFilter(account, dvmKey, requestId)) { class Factory( val account: Account, val dvmKey: String, val requestId: String, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun <T : ViewModel> create(modelClass: Class<T>): T = NIP90ContentDiscoveryFeedViewModel(account, dvmKey, requestId) as T + override fun <T : ViewModel> create(modelClass: Class<T>): T = DvmContentDiscoveryFeedViewModel(account, dvmKey, requestId) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryResponseFilter.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryResponseFilter.kt index acb70c2ab0..b9c8653a49 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryResponseFilter.kt @@ -29,7 +29,7 @@ import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent -open class NIP90ContentDiscoveryResponseFilter( +open class DvmContentDiscoveryResponseFilter( val account: Account, val dvmkey: String, val request: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/ListOfEmojiPacksScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/ListOfEmojiPacksScreen.kt index 3d1af6ad19..e385a772aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/ListOfEmojiPacksScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/list/ListOfEmojiPacksScreen.kt @@ -315,7 +315,7 @@ private fun EmojiListToggleRow( LoadAddressableNote(packAddress) { it?.let { packNote -> LoadAddressableNote( - accountViewModel.account.emoji.getEmojiPackSelectionAddress(), + accountViewModel.account.emoji.getEmojiListAddress(), ) { selectionNote -> selectionNote?.let { usersEmojiList -> val hasAddedThis by observeNoteAndMap(usersEmojiList, accountViewModel) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/EmojiPackSelectionScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/EmojiPackSelectionScreen.kt index 798e89e207..e3774aa912 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/EmojiPackSelectionScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/EmojiPackSelectionScreen.kt @@ -121,7 +121,7 @@ private fun EmojiPackSelectionBody( ) { item { LoadAddressableNote( - address = accountViewModel.account.emoji.getEmojiPackSelectionAddress(), + address = accountViewModel.account.emoji.getEmojiListAddress(), ) { selectionNote -> selectionNote?.let { val hasAddedThis by observeNoteAndMap(it, accountViewModel) { currentNote -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/MyEmojiListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/MyEmojiListScreen.kt index 6226d1a680..b23457dc69 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/MyEmojiListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/membershipManagement/MyEmojiListScreen.kt @@ -79,7 +79,7 @@ fun MyEmojiListScreen( nav: INav, ) { LoadAddressableNote( - address = accountViewModel.account.emoji.getEmojiPackSelectionAddress(), + address = accountViewModel.account.emoji.getEmojiListAddress(), ) { selectionNote -> selectionNote?.let { MyEmojiListView( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt index e72a91d271..cad3be3193 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/FollowPackFeedScreen.kt @@ -85,7 +85,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal.Follow import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal.FollowPackMembersUserFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource.FollowPackFeedFilterAssemblerSubscription import com.vitorpamplona.quartz.nip01Core.core.Address -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt index 663afb6994..2a56243e86 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt @@ -37,7 +37,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent import com.vitorpamplona.quartz.nipA4PublicMessages.PublicMessageEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt index c89f21f3d7..26a3f6bd6f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt @@ -46,7 +46,7 @@ import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt index a081c0031b..0e6b1ee160 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackMembersFeedFilter.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache.checkGetOrCreateUser import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent class FollowPackMembersFeedFilter( val followPackNote: AddressableNote, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterSubAssembler.kt index 7c7b968467..13c6fe051f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/datasource/FollowPackFeedFilterSubAssembler.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.home.nip65Follows.filterHo import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent class FollowPackFeedFilterSubAssembler( client: INostrClient, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksFeedLoaded.kt index 4ff1d00053..5937c2290a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksFeedLoaded.kt @@ -38,7 +38,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.ChannelCardCompose -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent @OptIn(ExperimentalFoundationApi::class) @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksTopBar.kt index 1c67d0241b..644b0e02b0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/FollowPacksTopBar.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar import com.vitorpamplona.amethyst.ui.screen.FeedDefinition import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent @Composable fun FollowPacksTopBar( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt index 8733e7a42f..dcc16a37a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/lists/PeopleListViewModel.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -57,7 +57,7 @@ class PeopleListViewModel : ViewModel() { selectedDTag .transformLatest { emitAll( - account.peopleLists.selectListFlow(it).flowOn(Dispatchers.IO), + account.followSets.selectListFlow(it).flowOn(Dispatchers.IO), ) }.flowOn(Dispatchers.IO) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null) @@ -79,23 +79,23 @@ class PeopleListViewModel : ViewModel() { } suspend fun deleteFollowSet() { - account.peopleLists.deleteFollowSet(selectedDTag.value, account) + account.followSets.deleteFollowSet(selectedDTag.value, account) } - fun loadNote(): AddressableNote? = account.peopleLists.getPeopleListNote(selectedDTag.value) + fun loadNote(): AddressableNote? = account.followSets.getPeopleListNote(selectedDTag.value) suspend fun removeUserFromSet( user: User, isPrivate: Boolean, ) { - account.peopleLists.removeUserFromSet(user, isPrivate, selectedDTag.value, account) + account.followSets.removeUserFromSet(user, isPrivate, selectedDTag.value, account) } suspend fun addUserToSet( user: User, isPrivate: Boolean, ) { - account.peopleLists.addUserToSet(user, selectedDTag.value, isPrivate, account) + account.followSets.addUserToSet(user, selectedDTag.value, isPrivate, account) } fun hasUserFlow( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt index f54fa456ca..002eb5f4a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/display/packs/FollowPackViewModel.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -57,7 +57,7 @@ class FollowPackViewModel : ViewModel() { selectedDTag .transformLatest { emitAll( - account.followLists.selectListFlow(it).flowOn(Dispatchers.IO), + account.starterPacks.selectListFlow(it).flowOn(Dispatchers.IO), ) }.flowOn(Dispatchers.IO) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null) @@ -79,17 +79,17 @@ class FollowPackViewModel : ViewModel() { } suspend fun deleteFollowSet() { - account.followLists.deleteFollowSet(selectedDTag.value, account) + account.starterPacks.deleteFollowSet(selectedDTag.value, account) } - fun loadNote(): AddressableNote? = account.followLists.getPeopleListNote(selectedDTag.value) + fun loadNote(): AddressableNote? = account.starterPacks.getPeopleListNote(selectedDTag.value) suspend fun removeUserFromSet(user: User) { - account.followLists.removeUserFromSet(user, selectedDTag.value, account) + account.starterPacks.removeUserFromSet(user, selectedDTag.value, account) } suspend fun addUserToSet(user: User) { - account.followLists.addUserToSet(user, selectedDTag.value, account) + account.starterPacks.addUserToSet(user, selectedDTag.value, account) } fun hasUserFlow(user: User): Flow<Boolean> = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/FollowPackViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/FollowPackViewModel.kt index ad08cf8121..a2bdd2eb2d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/FollowPackViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/FollowPackViewModel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel -import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleList +import com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets.PeopleList import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Stable @@ -33,7 +33,7 @@ class FollowPackViewModel : ViewModel() { this.accountViewModel = accountViewModel } - fun listFlow() = accountViewModel.account.followLists.uiListFlow + fun listFlow() = accountViewModel.account.starterPacks.uiListFlow fun cloneItem( followSet: PeopleList, @@ -41,7 +41,7 @@ class FollowPackViewModel : ViewModel() { customDescription: String?, ) { accountViewModel.launchSigner { - accountViewModel.account.followLists.cloneFollowSet( + accountViewModel.account.starterPacks.cloneFollowSet( currentFollowPack = followSet, customCloneName = customName, customCloneDescription = customDescription, @@ -52,7 +52,7 @@ class FollowPackViewModel : ViewModel() { fun deleteItem(followSet: PeopleList) { accountViewModel.launchSigner { - accountViewModel.account.followLists.deleteFollowSet( + accountViewModel.account.starterPacks.deleteFollowSet( identifierTag = followSet.identifierTag, account = accountViewModel.account, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListViewModel.kt index 94e853c494..808b175ada 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/PeopleListViewModel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel -import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleList +import com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets.PeopleList import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Stable @@ -33,7 +33,7 @@ class PeopleListViewModel : ViewModel() { this.accountViewModel = accountViewModel } - fun listFlow() = accountViewModel.account.peopleLists.uiListFlow + fun listFlow() = accountViewModel.account.followSets.uiListFlow fun cloneItem( followSet: PeopleList, @@ -41,7 +41,7 @@ class PeopleListViewModel : ViewModel() { customDescription: String?, ) { accountViewModel.launchSigner { - accountViewModel.account.peopleLists.cloneFollowSet( + accountViewModel.account.followSets.cloneFollowSet( currentPeopleList = followSet, customCloneName = customName, customCloneDescription = customDescription, @@ -52,7 +52,7 @@ class PeopleListViewModel : ViewModel() { fun deleteItem(followSet: PeopleList) { accountViewModel.launchSigner { - accountViewModel.account.peopleLists.deleteFollowSet( + accountViewModel.account.followSets.deleteFollowSet( identifierTag = followSet.identifierTag, account = accountViewModel.account, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt index 695e5e0985..89c0ab56f6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/FollowPackMetadataViewModel.kt @@ -31,7 +31,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType -import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleList +import com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets.PeopleList import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details @@ -83,7 +83,7 @@ class FollowPackMetadataViewModel : ViewModel() { } fun load(dTag: String) { - peopleList = account.followLists.selectList(dTag) + peopleList = account.starterPacks.selectList(dTag) name.value = TextFieldValue(peopleList?.title ?: "") picture.value = TextFieldValue(peopleList?.image ?: "") description.value = TextFieldValue(peopleList?.description ?: "") @@ -94,14 +94,14 @@ class FollowPackMetadataViewModel : ViewModel() { val peopleList = peopleList if (peopleList == null) { val newListIdentifier = - accountViewModel.account.followLists.addFollowList( + accountViewModel.account.starterPacks.addFollowList( name = name.value.text, desc = description.value.text, image = picture.value.text, account = accountViewModel.account, ) } else { - accountViewModel.account.followLists.updateMetadata( + accountViewModel.account.starterPacks.updateMetadata( name = name.value.text, desc = description.value.text, image = picture.value.text, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt index 9e5ebac387..70a193bb50 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/list/metadata/PeopleListMetadataViewModel.kt @@ -31,7 +31,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType -import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleList +import com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets.PeopleList import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.avif_metadata_strip_failed import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details @@ -83,7 +83,7 @@ class PeopleListMetadataViewModel : ViewModel() { } fun load(dTag: String) { - peopleList = account.peopleLists.selectList(dTag) + peopleList = account.followSets.selectList(dTag) name.value = TextFieldValue(peopleList?.title ?: "") picture.value = TextFieldValue(peopleList?.image ?: "") description.value = TextFieldValue(peopleList?.description ?: "") @@ -95,14 +95,14 @@ class PeopleListMetadataViewModel : ViewModel() { accountViewModel.launchSigner { val peopleList = peopleList if (peopleList == null) { - accountViewModel.account.peopleLists.addFollowList( + accountViewModel.account.followSets.addFollowList( listName = name.value.text, listDescription = description.value.text, listImage = picture.value.text, account = accountViewModel.account, ) } else { - accountViewModel.account.peopleLists.updateMetadata( + accountViewModel.account.followSets.updateMetadata( listName = name.value.text, listDescription = description.value.text, listImage = picture.value.text, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserView.kt index 7f61e68033..4dbd832a96 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/memberEdit/FollowListAndPackAndUserView.kt @@ -63,9 +63,9 @@ fun FollowListAndPackAndUserView( accountViewModel: AccountViewModel, nav: INav, ) { - val followSetsState by accountViewModel.account.peopleLists.uiListFlow + val followSetsState by accountViewModel.account.followSets.uiListFlow .collectAsStateWithLifecycle() - val followPackFeedState by accountViewModel.account.followLists.uiListFlow + val followPackFeedState by accountViewModel.account.starterPacks.uiListFlow .collectAsStateWithLifecycle() val userName by observeUserName(userToAddOrRemove, accountViewModel) @@ -114,7 +114,7 @@ fun FollowListAndPackAndUserView( userIsPublicMember = list.publicMembers.contains(userToAddOrRemove), onRemoveUser = { accountViewModel.launchSigner { - accountViewModel.account.peopleLists.removeUserFromSet( + accountViewModel.account.followSets.removeUserFromSet( userToAddOrRemove, isPrivate = list.privateMembers.contains(userToAddOrRemove), list.identifierTag, @@ -129,7 +129,7 @@ fun FollowListAndPackAndUserView( }, onAddUserToList = { userShouldBePrivate -> accountViewModel.launchSigner { - accountViewModel.account.peopleLists.addUserToSet( + accountViewModel.account.followSets.addUserToSet( userToAddOrRemove, list.identifierTag, userShouldBePrivate, @@ -189,7 +189,7 @@ fun FollowListAndPackAndUserView( }, onRemoveUser = { accountViewModel.launchSigner { - accountViewModel.account.followLists.removeUserFromSet( + accountViewModel.account.starterPacks.removeUserFromSet( userToAddOrRemove, list.identifierTag, accountViewModel.account, @@ -199,7 +199,7 @@ fun FollowListAndPackAndUserView( memberSize = list.publicMembers.size, onAddUserToList = { accountViewModel.launchSigner { - accountViewModel.account.followLists.addUserToSet( + accountViewModel.account.starterPacks.addUserToSet( userToAddOrRemove, list.identifierTag, accountViewModel.account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt index 8f5a8fe47f..ce6c0e9dc2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.number_followers import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsFollowerCount +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAssertionsFollowerCount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.dal.UserProfileFollowersUserFeedViewModel @@ -38,7 +38,7 @@ fun FollowersTabHeader( followersFeedViewModel: UserProfileFollowersUserFeedViewModel, accountViewModel: AccountViewModel, ) { - val followerCount by observeUserContactCardsFollowerCount(baseUser, accountViewModel) + val followerCount by observeUserAssertionsFollowerCount(baseUser, accountViewModel) if (followerCount == "--") { FollowersTabHeaderLocal(followersFeedViewModel, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserNicknameCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserNicknameCard.kt index e614a462df..5e666661c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserNicknameCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserNicknameCard.kt @@ -73,8 +73,8 @@ fun UserNicknameCard( WatchAndLoadMyEmojiList(accountViewModel) EditNicknameDialog( user = baseUser, - contactCards = accountViewModel.account.contactCards, - onSave = { petName, summary -> accountViewModel.updateContactCardPetName(baseUser, petName, summary) }, + userAssertions = accountViewModel.account.userAssertions, + onSave = { petName, summary -> accountViewModel.updateUserAssertionPetName(baseUser, petName, summary) }, onDismiss = { isEditDialogOpen.value = false }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt index 07a9bc477b..78f5d38f66 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt @@ -71,8 +71,8 @@ fun UserProfileDropDownMenu( WatchAndLoadMyEmojiList(accountViewModel) EditNicknameDialog( user = user, - contactCards = accountViewModel.account.contactCards, - onSave = { petName, summary -> accountViewModel.updateContactCardPetName(user, petName, summary) }, + userAssertions = accountViewModel.account.userAssertions, + onSave = { petName, summary -> accountViewModel.updateUserAssertionPetName(user, petName, summary) }, onDismiss = { isNicknameDialogOpen.value = false }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index 51a2e0107d..42082044f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -109,7 +109,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.ConnectedR import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.renderConnectedItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.DMRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.renderDMItems -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.feeds.RelayFeedsListViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.feeds.FavoriteRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.feeds.renderRelayFeedsItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.indexer.IndexerRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.indexer.renderIndexerItems @@ -148,7 +148,7 @@ fun AllRelayListScreen( val broadcastViewModel: BroadcastRelayListViewModel = viewModel() val indexerViewModel: IndexerRelayListViewModel = viewModel() val proxyViewModel: ProxyRelayListViewModel = viewModel() - val relayFeedsViewModel: RelayFeedsListViewModel = viewModel() + val relayFeedsViewModel: FavoriteRelayListViewModel = viewModel() dmViewModel.init(accountViewModel) keyPackageViewModel.init(accountViewModel) @@ -214,7 +214,7 @@ fun MappedAllRelayListView( broadcastViewModel: BroadcastRelayListViewModel, indexerViewModel: IndexerRelayListViewModel, proxyViewModel: ProxyRelayListViewModel, - relayFeedsViewModel: RelayFeedsListViewModel, + relayFeedsViewModel: FavoriteRelayListViewModel, accountViewModel: AccountViewModel, nav: INav, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/KindDisplayName.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/KindDisplayName.kt index 0684f76185..c8b6e75aaf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/KindDisplayName.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/KindDisplayName.kt @@ -248,12 +248,11 @@ import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.BookmarkSetEvent +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent @@ -261,6 +260,7 @@ import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip51Lists.videoCurationSet.VideoCurationSetEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent @@ -274,7 +274,7 @@ import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresence import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent @@ -434,7 +434,7 @@ fun kindDisplayName(kind: Int): StringResource? = Bolt12ZapEvent.KIND -> Res.string.kind_zaps NwcRequestEvent.KIND -> Res.string.kind_nwc_request NwcResponseEvent.KIND -> Res.string.kind_nwc_response - LnZapPrivateEvent.KIND -> Res.string.kind_private_zaps + PrivateZapEvent.KIND -> Res.string.kind_private_zaps ZapRequestEvent.KIND -> Res.string.kind_zap_req LongFormContentEvent.KIND -> Res.string.kind_blogs MeetingRoomEvent.KIND -> Res.string.kind_meeting_room diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/FavoriteRelayListViewModel.kt similarity index 90% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListViewModel.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/FavoriteRelayListViewModel.kt index aab6d86fd0..91fd7cf9a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/FavoriteRelayListViewModel.kt @@ -25,12 +25,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Stable -class RelayFeedsListViewModel : BasicRelaySetupInfoModel() { +class FavoriteRelayListViewModel : BasicRelaySetupInfoModel() { override fun getRelayList(): List<NormalizedRelayUrl> = - account.relayFeedsList.flow.value + account.favoriteRelayList.flow.value .toList() override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) { - account.saveRelayFeedsList(urlList) + account.saveFavoriteRelayList(urlList) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt index 2c77021427..cf07d2053d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/feeds/RelayFeedsListView.kt @@ -45,8 +45,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditF import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder @Composable -fun RelayFeedsList( - postViewModel: RelayFeedsListViewModel, +fun FavoriteRelayList( + postViewModel: FavoriteRelayListViewModel, accountViewModel: AccountViewModel, onClose: () -> Unit, nav: INav, @@ -71,7 +71,7 @@ fun RelayFeedsList( fun LazyListScope.renderRelayFeedsItems( feedState: List<BasicRelaySetupInfo>, - postViewModel: RelayFeedsListViewModel, + postViewModel: FavoriteRelayListViewModel, accountViewModel: AccountViewModel, nav: INav, dragState: RelayDragState? = null, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 74d6155773..75050af4ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -176,17 +176,17 @@ import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayBroadcastRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayContactList import com.vitorpamplona.amethyst.ui.note.types.DisplayDMRelayList -import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList +import com.vitorpamplona.amethyst.ui.note.types.DisplayFavoriteRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayIndexerRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayPeopleList import com.vitorpamplona.amethyst.ui.note.types.DisplayProxyRelayList -import com.vitorpamplona.amethyst.ui.note.types.DisplayRelayFeedsList import com.vitorpamplona.amethyst.ui.note.types.DisplayRelaySet import com.vitorpamplona.amethyst.ui.note.types.DisplaySearchRelayList +import com.vitorpamplona.amethyst.ui.note.types.DisplayStarterPack import com.vitorpamplona.amethyst.ui.note.types.DisplayTrustedRelayList import com.vitorpamplona.amethyst.ui.note.types.EditState -import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay +import com.vitorpamplona.amethyst.ui.note.types.FileMetadataDisplay import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay import com.vitorpamplona.amethyst.ui.note.types.MAX_THREAD_SECTIONS import com.vitorpamplona.amethyst.ui.note.types.PictureDisplay @@ -223,7 +223,6 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderHighlight import com.vitorpamplona.amethyst.ui.note.types.RenderInteractiveStory import com.vitorpamplona.amethyst.ui.note.types.RenderLearningResource import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityChatMessage -import com.vitorpamplona.amethyst.ui.note.types.RenderLnZap import com.vitorpamplona.amethyst.ui.note.types.RenderMeetingRoomEvent import com.vitorpamplona.amethyst.ui.note.types.RenderMeetingSpaceEvent import com.vitorpamplona.amethyst.ui.note.types.RenderMusicPlaylist @@ -263,6 +262,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderTextModificationEvent import com.vitorpamplona.amethyst.ui.note.types.RenderTorrent import com.vitorpamplona.amethyst.ui.note.types.RenderTorrentComment import com.vitorpamplona.amethyst.ui.note.types.RenderZapPoll +import com.vitorpamplona.amethyst.ui.note.types.RenderZapReceipt import com.vitorpamplona.amethyst.ui.note.types.ReplyRenderType import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay import com.vitorpamplona.amethyst.ui.note.types.VoiceHeader @@ -343,8 +343,7 @@ import com.vitorpamplona.quartz.nip43RelayMembers.list.RelayMembershipListEvent import com.vitorpamplona.quartz.nip43RelayMembers.removeMember.RelayRemoveMemberEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent @@ -353,6 +352,7 @@ import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent @@ -897,13 +897,13 @@ private fun FullBleedNoteCompose( } else if (noteEvent is BaseVoiceEvent) { VoiceHeader(noteEvent, baseNote, accountViewModel, nav) } else if (noteEvent is FileMetadataEvent) { - FileHeaderDisplay(baseNote, roundedCorner = true, ContentScale.FillWidth, accountViewModel = accountViewModel) + FileMetadataDisplay(baseNote, roundedCorner = true, ContentScale.FillWidth, accountViewModel = accountViewModel) } else if (noteEvent is FileStorageHeaderEvent) { FileStorageHeaderDisplay(baseNote, roundedCorner = true, ContentScale.FillWidth, accountViewModel = accountViewModel) } else if (noteEvent is FollowSetEvent) { DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is StarterPackEvent) { - DisplayFollowList(baseNote, false, accountViewModel, nav) + DisplayStarterPack(baseNote, false, accountViewModel, nav) } else if (noteEvent is ContactListEvent) { DisplayContactList(baseNote, accountViewModel, nav) } else if (noteEvent is AudioTrackEvent) { @@ -978,7 +978,7 @@ private fun FullBleedNoteCompose( } else if (noteEvent is AdvertisedRelayListEvent) { DisplayNIP65RelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is ZapReceiptEvent) { - RenderLnZap(baseNote, quotesLeft = 3, backgroundColor = backgroundColor, accountViewModel = accountViewModel, nav = nav) + RenderZapReceipt(baseNote, quotesLeft = 3, backgroundColor = backgroundColor, accountViewModel = accountViewModel, nav = nav) } else if (noteEvent is NutzapEvent) { RenderNutzap(baseNote, quotesLeft = 3, backgroundColor = backgroundColor, accountViewModel = accountViewModel, nav = nav) } else if (noteEvent is OnchainZapEvent) { @@ -996,7 +996,7 @@ private fun FullBleedNoteCompose( } else if (noteEvent is TrustedRelayListEvent) { DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is FavoriteRelayListEvent) { - DisplayRelayFeedsList(baseNote, backgroundColor, accountViewModel, nav) + DisplayFavoriteRelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is IndexerRelayListEvent) { DisplayIndexerRelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is BroadcastRelayListEvent) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileHeaderCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileMetadataCardCompose.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileHeaderCardCompose.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileMetadataCardCompose.kt index 523cb12efd..4b74c1ddf4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileHeaderCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileMetadataCardCompose.kt @@ -47,7 +47,7 @@ import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.components.collectContentWarningReasons import com.vitorpamplona.amethyst.ui.components.mediaSizingModifier import com.vitorpamplona.amethyst.ui.note.ReactionsRow -import com.vitorpamplona.amethyst.ui.note.types.FileHeaderAttachmentCard +import com.vitorpamplona.amethyst.ui.note.types.FileMetadataAttachmentCard import com.vitorpamplona.amethyst.ui.note.types.toMediaContent import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Event @@ -55,7 +55,7 @@ import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip94FileMetadata.FileMetadataEvent @Composable -fun FileHeaderCardCompose( +fun FileMetadataCardCompose( baseNote: Note, accountViewModel: AccountViewModel, nav: INav, @@ -70,7 +70,7 @@ fun FileHeaderCardCompose( UserCardHeader(baseNote, accountViewModel, nav) // Image content - FileHeaderCardImage(baseNote, event, backgroundColor, accountViewModel) + FileMetadataCardImage(baseNote, event, backgroundColor, accountViewModel) // Reactions row ReactionsRow( @@ -83,12 +83,12 @@ fun FileHeaderCardCompose( ) // Title and content - FileHeaderCardCaption(event) + FileMetadataCardCaption(event) } } @Composable -private fun FileHeaderCardImage( +private fun FileMetadataCardImage( note: Note, event: FileMetadataEvent, backgroundColor: MutableState<Color>, @@ -108,7 +108,7 @@ private fun FileHeaderCardImage( // Reachable despite VideoFeedFilter admitting only image/video types: the filter accepts on // `urls().any { … }` while this card renders `url()`, the first tag — so a multi-mirror event // whose first URL is unrenderable lands here. The gate wraps it for the same reason it wraps - // the viewer in FileHeaderDisplay: a content warning is about the file, and the card still + // the viewer in FileMetadataDisplay: a content warning is about the file, and the card still // spells out its filename, alt text, MIME and size. Sizing stays on the gate's defaults // (fillMaxWidth, no backdrop) — a link card has no aspect ratio to reserve and no blurhash // to show behind it. @@ -119,7 +119,7 @@ private fun FileHeaderCardImage( preloadUrls = emptyList(), accountViewModel = accountViewModel, ) { - FileHeaderAttachmentCard(event, fullUrl, mimeType) + FileMetadataAttachmentCard(event, fullUrl, mimeType) } return } @@ -145,7 +145,7 @@ private fun FileHeaderCardImage( } @Composable -internal fun FileHeaderCardCaption(videoEvent: FileMetadataEvent) { +internal fun FileMetadataCardCaption(videoEvent: FileMetadataEvent) { val event = (videoEvent as? Event) ?: return val title = videoEvent.summary() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt index d9b3fe531b..7615245745 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt @@ -200,7 +200,7 @@ fun VideoFeedLoaded( } item.event is FileMetadataEvent -> { - FileHeaderCardCompose(item, accountViewModel, nav) + FileMetadataCardCompose(item, accountViewModel, nav) HorizontalDivider( thickness = DividerThickness, diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/resourceusage/ResourceUsageLedgerTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/resourceusage/ResourceUsageLedgerTest.kt index 63bbe4a78b..6f0365319d 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/resourceusage/ResourceUsageLedgerTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/resourceusage/ResourceUsageLedgerTest.kt @@ -43,7 +43,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import io.mockk.every import io.mockk.mockk @@ -650,7 +650,7 @@ class MeteringNostrSignerTest { fromPublicKey: HexKey, ) = "dec44" - override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = throw UnsupportedOperationException("fake") + override suspend fun decryptZapEvent(event: ZapRequestEvent): PrivateZapEvent = throw UnsupportedOperationException("fake") override suspend fun deriveKey(nonce: HexKey): HexKey = "bb".repeat(32) diff --git a/cli/plans/archive/2026-04-23-nip17-dm.md b/cli/plans/archive/2026-04-23-nip17-dm.md index 28918e37e2..7a46443a45 100644 --- a/cli/plans/archive/2026-04-23-nip17-dm.md +++ b/cli/plans/archive/2026-04-23-nip17-dm.md @@ -42,7 +42,7 @@ DMs); on `list`/`await` it filters by counter-party. | Build `kind:14` chat message template | `ChatMessageEvent.build(msg, to, …)` | `quartz/.../nip17Dm/messages/ChatMessageEvent.kt:51` | | Sign + seal + wrap (one wrap per recipient incl. self) | `NIP17Factory().createMessageNIP17(template, signer)` | `quartz/.../nip17Dm/NIP17Factory.kt:37` | | `GiftWrapEvent.create / unwrapOrNull / recipientPubKey` | `GiftWrapEvent` (kind 1059) | `quartz/.../nip59Giftwrap/wraps/GiftWrapEvent.kt` | -| Inner seal layer (kind 13) | `SealedRumorEvent` | `quartz/.../nip59Giftwrap/seals/SealedRumorEvent.kt` | +| Inner seal layer (kind 13) | `SealEvent` | `quartz/.../nip59Giftwrap/seals/SealEvent.kt` | | NIP-44 v2 encrypt/decrypt (called from wrap/seal) | `Nip44` singleton | `quartz/.../nip44Encryption/Nip44.kt` | | Resolve recipient relay lists (10050 / 10051 / 10002) | `RecipientRelayFetcher.fetchRelayLists` | `quartz/.../marmot/RecipientRelayFetcher.kt:80` | | Publish + per-relay ACK | `Context.publish(event, relays)` | `cli/.../Context.kt:154` | @@ -58,7 +58,7 @@ Everything above is already on the JVM classpath of the `cli` module. | Piece | Status | Current | Target | Notes | |---|---|---|---|---| -| `NIP17Factory`, `GiftWrapEvent`, `SealedRumorEvent`, `Nip44`, `ChatMessageEvent` | ✅ Reuse | `quartz/` | — | Pure protocol; CLI calls directly. | +| `NIP17Factory`, `GiftWrapEvent`, `SealEvent`, `Nip44`, `ChatMessageEvent` | ✅ Reuse | `quartz/` | — | Pure protocol; CLI calls directly. | | `RecipientRelayFetcher.fetchRelayLists` | ✅ Reuse | `quartz/.../marmot/` | — | Already used by `AwaitCommands.awaitKeyPackage`; same call shape. | | `filterGiftWrapsToPubkey` | 📦 Extract | `amethyst/.../service/relayClient/reqCommand/account/nip59GiftWraps/FilterGiftWrapsToPubkey.kt:31` | `commons/.../relayClient/nip17Dm/FilterGiftWrapsToPubkey.kt` | Pure function, no Android deps. Update Android caller (`AccountGiftWrapsEoseManager.kt:55`) in the same commit. | | Per-recipient relay routing (kind:10050 → 10002 read → bootstrap) | 🆕 New (in CLI) | inline inside `Account.computeRelayListToBroadcast` (`amethyst/.../model/Account.kt:908`) | `cli/.../commands/DmCommands.kt` (private helper) | Tiny — three-line fallback. Promoting it to commons can wait until a second consumer appears. | @@ -133,7 +133,7 @@ raw = ctx.drain(filters, timeoutMs = --timeout * 1000 ?: 8_000) ↓ messages = raw.mapNotNull { (relay, event) -> if (event !is GiftWrapEvent) return@mapNotNull null - val sealed = event.unwrapOrNull(ctx.signer) as? SealedRumorEvent ?: return@mapNotNull null + val sealed = event.unwrapOrNull(ctx.signer) as? SealEvent ?: return@mapNotNull null val inner = sealed.unsealOrNull(ctx.signer) as? ChatMessageEvent ?: return@mapNotNull null DecryptedMsg(inner, relay, event.id) }.dedupBy { it.inner.id } diff --git a/commons/plans/2026-05-04-custom-feeds-plan.md b/commons/plans/2026-05-04-custom-feeds-plan.md index 1a54116a6a..f572a0d9cf 100644 --- a/commons/plans/2026-05-04-custom-feeds-plan.md +++ b/commons/plans/2026-05-04-custom-feeds-plan.md @@ -463,7 +463,7 @@ class FeedBuilderState(initial: FeedDefinition?) { - DVM request goes to DVM's advertised relays (from kind 31990 `relay` tags) - Response subscription listens on both user's relays AND DVM's relays - Use `MetadataPreloader` for bulk-fetching author metadata of returned notes -- Reuse existing `NIP90ContentDiscoveryRequestEvent.build()` pattern from Quartz +- Reuse existing `DvmContentDiscoveryRequestEvent.build()` pattern from Quartz ### Phase 7: Publish/Import (kind 31890) @@ -532,7 +532,7 @@ commons/src/commonTest/kotlin/.../feeds/custom/ | `TopFilter` | `amethyst/.../AccountSettings.kt` | Reference; `FeedSource.toTopFilter()` for bridge | | `FavoriteAlgoFeedsOrchestrator` | `amethyst/.../algoFeeds/` | Extract to commons for DVM reuse | | `FavoriteAlgoFeedsListEvent` (kind 10090) | `quartz/.../nip51Lists/` | Pinned feed sync | -| `NIP90ContentDiscoveryRequestEvent` | `quartz/.../nip90Dvms/` | DVM request building | +| `DvmContentDiscoveryRequestEvent` | `quartz/.../nip90Dvms/` | DVM request building | | `AppDefinitionEvent` (kind 31990) | `quartz/.../nip89AppHandlers/` | DVM marketplace discovery | | `NAddress` | `quartz/.../nip19Bech32/entities/` | naddr encode/decode | | `Nip19Parser` | `quartz/.../nip19Bech32/` | Detect pasted naddr | @@ -542,7 +542,7 @@ commons/src/commonTest/kotlin/.../feeds/custom/ | `PinnedNavBarState` | `desktopApp/.../deck/PinnedNavBarState.kt` | Reference pattern for pin state | | `HomeFeed` rendering | `desktopApp/.../home/` | Phase 2 content reuse | | `BaseAddressableEvent` | `quartz/.../nip01Core/core/` | Base for kind 31890 | -| `PeopleListEvent` / `InterestSetEvent` | `quartz/.../nip51Lists/` | Resolve ATag -> members | +| `FollowSetEvent` / `InterestSetEvent` | `quartz/.../nip51Lists/` | Resolve ATag -> members | | `MetadataPreloader` | `commons/.../relayClient/` | Bulk metadata fetch for feed results | | `FeedMetadataCoordinator` | `commons/.../relayClient/` | Coordinate metadata for visible notes | | `ComposeSubscriptionManager` | `commons/.../relayClient/` | Subscription lifecycle | diff --git a/commons/plans/2026-08-30-commons-migration-sweep.md b/commons/plans/2026-08-30-commons-migration-sweep.md index 20982ddae2..4d148d22a9 100644 --- a/commons/plans/2026-08-30-commons-migration-sweep.md +++ b/commons/plans/2026-08-30-commons-migration-sweep.md @@ -934,7 +934,7 @@ measured picture. are handled; the 9 without a dedicated `EventCache` overload (`TextNoteEvent`, `ContactListEvent`, `CommentEvent`, `AdvertisedRelayListEvent`, `BlossomServersEvent`, `BookmarkListEvent`, - `OldBookmarkListEvent`, `ChatMessageRelayListEvent`, `FollowListEvent`) fall + `OldBookmarkListEvent`, `DmRelayListEvent`, `StarterPackEvent`) fall into the generic replaceable/addressable group. - **13 of 13 core read methods match** by name and signature. - **Feed retention is already aligned** — both platforms hold feed content @@ -982,7 +982,7 @@ a deletion: ~500 lines of genuinely Desktop-specific state survive, and the concentrated in five places (metadata, contact list, follow pack, live activity, `clear`) and re-derive cleanly from the event after consume. 4. **Two Desktop-shaped `consume` overloads do not generalize** — the NIP-47 - `LnZapPaymentRequestEvent` one takes a `zappedNote` and an `onResponse` + `NwcRequestEvent` one takes a `zappedNote` and an `onResponse` callback, and the response one drives `paymentTracker` + `appScope`. Android reaches the same tracker through account state. Keep them Desktop-side. 5. **`clear()`** has only 2 production call sites (`Main.kt`, logout/account diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/account/AccountBootstrapEvents.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/account/AccountBootstrapEvents.kt index f5e80b7685..07b0376e67 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/account/AccountBootstrapEvents.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/account/AccountBootstrapEvents.kt @@ -62,8 +62,8 @@ data class AccountBootstrapEvents( val keyPackageRelayList: KeyPackageRelayListEvent, val searchRelayList: SearchRelayListEvent, val indexerRelayList: IndexerRelayListEvent, - val channelList: PublicChatListEvent, - val relayFeedsList: FavoriteRelayListEvent, + val publicChatList: PublicChatListEvent, + val favoriteRelayList: FavoriteRelayListEvent, ) { /** All nine signed events in publication order. */ fun all(): List<com.vitorpamplona.quartz.nip01Core.core.Event> = @@ -75,8 +75,8 @@ data class AccountBootstrapEvents( keyPackageRelayList, searchRelayList, indexerRelayList, - channelList, - relayFeedsList, + publicChatList, + favoriteRelayList, ) } @@ -109,6 +109,6 @@ fun bootstrapAccountEvents( keyPackageRelayList = KeyPackageRelayListEvent.create(DefaultNIP65RelaySet.toList(), signer), searchRelayList = SearchRelayListEvent.create(DefaultSearchRelayList.toList(), signer), indexerRelayList = IndexerRelayListEvent.create(DefaultIndexerRelayList.toList(), signer), - channelList = PublicChatListEvent.create(emptyList(), DefaultChannels, signer), - relayFeedsList = FavoriteRelayListEvent.create(DefaultGlobalRelays, signer), + publicChatList = PublicChatListEvent.create(emptyList(), DefaultChannels, signer), + favoriteRelayList = FavoriteRelayListEvent.create(DefaultGlobalRelays, signer), ) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/backups/ReplaceableBackupConflict.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/backups/ReplaceableBackupConflict.kt index f32456f1c1..b1ffb3334d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/backups/ReplaceableBackupConflict.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/backups/ReplaceableBackupConflict.kt @@ -39,7 +39,7 @@ import com.vitorpamplona.quartz.nip40Expiration.ExpirationTag import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt index 1789346dcf..0954ef7a9e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/EventCache.kt @@ -290,14 +290,13 @@ import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.BookmarkSetEvent import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent @@ -307,6 +306,7 @@ import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip51Lists.videoCurationSet.VideoCurationSetEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent @@ -327,9 +327,9 @@ import com.vitorpamplona.quartz.nip54Wiki.WikiRedirectEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent -import com.vitorpamplona.quartz.nip57Zaps.validate.LnZapReceiptValidator import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlEndpointResolver import com.vitorpamplona.quartz.nip57Zaps.validate.LnurlForm +import com.vitorpamplona.quartz.nip57Zaps.validate.ZapReceiptValidator import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent @@ -2252,7 +2252,7 @@ open class EventCache : val new = consumeBaseReplaceable(event, relay, wasVerified) if (relay != null && isRelaySignedGroupEvent(event, relay)) { val latest = getOrCreateAddressableNote(event.address()).event as? GroupRolesEvent - latest?.let { getOrCreateRelayGroupChannel(GroupId(it.groupId(), relay)).updateSupportedRoles(it) } + latest?.let { getOrCreateRelayGroupChannel(GroupId(it.groupId(), relay)).updateGroupRoles(it) } } return new } @@ -2712,18 +2712,18 @@ open class EventCache : if (cachedInfo != null) { val result = - LnZapReceiptValidator.validate( + ZapReceiptValidator.validate( receipt = event, expectedNostrPubkey = cachedInfo.nostrPubkey, expectedLnurl = recipientLnurl, ) - if (result is LnZapReceiptValidator.Result.Invalid && - result.reason != LnZapReceiptValidator.Result.Reason.MISMATCHED_LNURL + if (result is ZapReceiptValidator.Result.Invalid && + result.reason != ZapReceiptValidator.Result.Reason.MISMATCHED_LNURL ) { Log.w("ZP") { "dropping zap receipt ${event.id}: ${result.reason} ${result.detail ?: ""}" } return false } - if (result is LnZapReceiptValidator.Result.Invalid) { + if (result is ZapReceiptValidator.Result.Invalid) { Log.w("ZP") { "zap receipt ${event.id} has mismatched lnurl tag (accepting per SHOULD)" } } @@ -2759,18 +2759,18 @@ open class EventCache : return@launch } val result = - LnZapReceiptValidator.validate( + ZapReceiptValidator.validate( receipt = event, expectedNostrPubkey = info.nostrPubkey, expectedLnurl = recipientLnurl, ) - if (result is LnZapReceiptValidator.Result.Invalid && - result.reason != LnZapReceiptValidator.Result.Reason.MISMATCHED_LNURL + if (result is ZapReceiptValidator.Result.Invalid && + result.reason != ZapReceiptValidator.Result.Reason.MISMATCHED_LNURL ) { Log.w("ZP") { "dropping zap receipt ${event.id}: ${result.reason} ${result.detail ?: ""}" } return@launch } - if (result is LnZapReceiptValidator.Result.Invalid) { + if (result is ZapReceiptValidator.Result.Invalid) { Log.w("ZP") { "zap receipt ${event.id} has mismatched lnurl tag (accepting per SHOULD)" } } repliesTo.forEach { it.addZap(zapRequest, note) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt index 1f70bcea4b..da9e804fcb 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt @@ -44,9 +44,9 @@ import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch interface PublicChatListRepository { - fun channelList(): PublicChatListEvent? + fun publicChatList(): PublicChatListEvent? - fun updateChannelListTo(newChannelList: PublicChatListEvent?) + fun updatePublicChatListTo(newPublicChatList: PublicChatListEvent?) } class PublicChatListState( @@ -57,22 +57,22 @@ class PublicChatListState( val settings: PublicChatListRepository, ) { // Creates a long-term reference for this note so that the GC doesn't collect the note it self - val publicChatListNote = cache.getOrCreateAddressableNote(getChannelListAddress()) + val publicChatListNote = cache.getOrCreateAddressableNote(getPublicChatListAddress()) - fun getChannelListAddress() = PublicChatListEvent.createAddress(signer.pubKey) + fun getPublicChatListAddress() = PublicChatListEvent.createAddress(signer.pubKey) - fun getChannelListFlow(): StateFlow<NoteState> = publicChatListNote.flow().metadata.stateFlow + fun getPublicChatListFlow(): StateFlow<NoteState> = publicChatListNote.flow().metadata.stateFlow - fun getChannelList(): PublicChatListEvent? = publicChatListNote.event as? PublicChatListEvent + fun getPublicChatList(): PublicChatListEvent? = publicChatListNote.event as? PublicChatListEvent suspend fun publicChatListWithBackup(note: Note): Set<ChannelTag> { - val event = note.event as? PublicChatListEvent ?: settings.channelList() + val event = note.event as? PublicChatListEvent ?: settings.publicChatList() return event?.let { decryptionCache.channelSet(it) } ?: emptySet() } @OptIn(ExperimentalCoroutinesApi::class) val flow: StateFlow<Set<ChannelTag>> = - getChannelListFlow() + getPublicChatListFlow() .transformLatest { noteState -> emit(publicChatListWithBackup(noteState.note)) }.onStart { @@ -112,7 +112,7 @@ class PublicChatListState( ) suspend fun follow(channel: PublicChatChannel): PublicChatListEvent { - val publicChatList = getChannelList() + val publicChatList = getPublicChatList() return if (publicChatList == null) { PublicChatListEvent.create(ChannelTag(channel.idHex, channel.relayHintUrl()), true, signer) @@ -122,7 +122,7 @@ class PublicChatListState( } suspend fun follow(channels: List<PublicChatChannel>): PublicChatListEvent { - val publicChatList = getChannelList() + val publicChatList = getPublicChatList() val channelTags = channels.map { ChannelTag(it.idHex, it.relayHintUrl()) } return if (publicChatList == null) { @@ -133,7 +133,7 @@ class PublicChatListState( } suspend fun unfollow(channel: PublicChatChannel): PublicChatListEvent? { - val publicChatList = getChannelList() + val publicChatList = getPublicChatList() return if (publicChatList != null) { PublicChatListEvent.remove(publicChatList, ChannelTag(channel.idHex, channel.relayHintUrl()), signer) @@ -143,7 +143,7 @@ class PublicChatListState( } init { - settings.channelList()?.let { event -> + settings.publicChatList()?.let { event -> Log.d("AccountRegisterObservers") { "Loading saved channel list ${event.toJson()}" } @OptIn(DelicateCoroutinesApi::class) scope.launch(Dispatchers.IO) { @@ -153,10 +153,10 @@ class PublicChatListState( scope.launch(Dispatchers.IO) { Log.d("AccountRegisterObservers", "Channel List Collector Start") - getChannelListFlow().collect { + getPublicChatListFlow().collect { Log.d("AccountRegisterObservers") { "Channel List for ${signer.pubKey}" } (it.note.event as? PublicChatListEvent)?.let { - settings.updateChannelListTo(it) + settings.updatePublicChatListTo(it) } } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/RelayGroupChannel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/RelayGroupChannel.kt index d1009af4cc..7849dfd3d0 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/RelayGroupChannel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/RelayGroupChannel.kt @@ -109,9 +109,9 @@ class RelayGroupChannel( * the role names a moderation UI should offer when assigning a role, since the exact * set is relay-defined (NIP-29 §Group management). */ - var supportedRoles: List<RoleTag> = emptyList() + var groupRoles: List<RoleTag> = emptyList() private set - private var supportedRolesUpdatedAt: Long = 0 + private var groupRolesUpdatedAt: Long = 0 /** * Members ∪ admins, recomputed only when a roster event lands. [memberCount] and the discovery @@ -164,7 +164,7 @@ class RelayGroupChannel( event != null || members.isNotEmpty() || admins.isNotEmpty() || - supportedRoles.isNotEmpty() || + groupRoles.isNotEmpty() || pinnedEventIds.isNotEmpty() /** A relay group lives on exactly one relay: its host. */ @@ -237,11 +237,11 @@ class RelayGroupChannel( updateChannelInfo() } - fun updateSupportedRoles(event: GroupRolesEvent) { + fun updateGroupRoles(event: GroupRolesEvent) { // Only newer definitions supersede; equal-or-older is dropped (no redundant emit). - if (event.createdAt <= supportedRolesUpdatedAt) return - supportedRoles = event.roles() - supportedRolesUpdatedAt = event.createdAt + if (event.createdAt <= groupRolesUpdatedAt) return + groupRoles = event.roles() + groupRolesUpdatedAt = event.createdAt updateChannelInfo() } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiPackState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiPackState.kt index 612e2c5673..eea948a198 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiPackState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiPackState.kt @@ -53,13 +53,13 @@ class EmojiPackState( ) // Creates a long-term reference for this note so that the GC doesn't collect the note it self - val emojiPackListNote = cache.getOrCreateAddressableNote(getEmojiPackSelectionAddress()) + val emojiPackListNote = cache.getOrCreateAddressableNote(getEmojiListAddress()) - fun getEmojiPackSelectionAddress() = EmojiListEvent.createAddress(signer.pubKey) + fun getEmojiListAddress() = EmojiListEvent.createAddress(signer.pubKey) - fun getEmojiPackSelection(): EmojiListEvent? = emojiPackListNote.event as? EmojiListEvent + fun getEmojiList(): EmojiListEvent? = emojiPackListNote.event as? EmojiListEvent - fun getEmojiPackSelectionFlow(): StateFlow<NoteState> = emojiPackListNote.flow().metadata.stateFlow + fun getEmojiListFlow(): StateFlow<NoteState> = emojiPackListNote.flow().metadata.stateFlow fun convertEmojiSelectionPack(selection: EmojiListEvent?): List<StateFlow<NoteState>>? = selection?.taggedAddresses()?.map { @@ -71,11 +71,11 @@ class EmojiPackState( @OptIn(ExperimentalCoroutinesApi::class) val flow: StateFlow<List<StateFlow<NoteState>>?> = - getEmojiPackSelectionFlow() + getEmojiListFlow() .transformLatest { emit(convertEmojiSelectionPack(it.note.event as? EmojiListEvent)) }.onStart { - emit(convertEmojiSelectionPack(getEmojiPackSelection())) + emit(convertEmojiSelectionPack(getEmojiList())) }.flowOn(Dispatchers.IO) .stateIn( scope, @@ -115,7 +115,7 @@ class EmojiPackState( emit( mergePackWithPrivate( convertEmojiSelectionPack( - getEmojiPackSelection(), + getEmojiList(), )?.map { it.value }?.toTypedArray() ?: emptyArray(), ), ) @@ -149,7 +149,7 @@ class EmojiPackState( val eventHint = emojiPack.toEventHint<EmojiPackEvent>() ?: throw IllegalArgumentException("Cannot build event hint for this emoji pack.") - val usersEmojiList = getEmojiPackSelection() + val usersEmojiList = getEmojiList() return if (usersEmojiList == null) { val template = EmojiListEvent.build(listOf(eventHint)) signer.sign(template) @@ -160,7 +160,7 @@ class EmojiPackState( } suspend fun removeEmojiPack(emojiPack: Note): EmojiListEvent? { - val usersEmojiList = getEmojiPackSelection() ?: throw IllegalArgumentException("No emoji pack selection exists to remove from.") + val usersEmojiList = getEmojiList() ?: throw IllegalArgumentException("No emoji pack selection exists to remove from.") val emojiPackEvent = emojiPack.event if (emojiPackEvent !is EmojiPackEvent) return null diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt index 0d36cb6391..e0c991c18a 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt @@ -23,12 +23,12 @@ package com.vitorpamplona.amethyst.commons.model.nip51Lists.blockPeopleList import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.commons.model.cache.LocalCache -import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleListDecryptionCache +import com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets.FollowSetDecryptionCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag import com.vitorpamplona.quartz.nip51Lists.muteList.tags.WordTag -import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO @@ -42,7 +42,7 @@ import kotlinx.coroutines.flow.stateIn class BlockPeopleListState( val signer: NostrSigner, val cache: LocalCache, - val decryptionCache: PeopleListDecryptionCache, + val decryptionCache: FollowSetDecryptionCache, val scope: CoroutineScope, ) { // Creates a long-term reference for this note so that the GC doesn't collect the note it self diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/bookmarkSets/BookmarkSet.kt similarity index 94% rename from commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/bookmarkSets/BookmarkSet.kt index a3e8e03d70..3187738854 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkList.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/bookmarkSets/BookmarkSet.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists +package com.vitorpamplona.amethyst.commons.model.nip51Lists.bookmarkSets import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark @Stable -data class LabeledBookmarkList( +data class BookmarkSet( val identifier: String, val title: String, val description: String?, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/relayFeeds/RelayFeedsListDecryptionCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/favoriteRelays/FavoriteRelayListDecryptionCache.kt similarity index 92% rename from commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/relayFeeds/RelayFeedsListDecryptionCache.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/favoriteRelays/FavoriteRelayListDecryptionCache.kt index 694897a993..65f7dc608e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/relayFeeds/RelayFeedsListDecryptionCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/favoriteRelays/FavoriteRelayListDecryptionCache.kt @@ -18,12 +18,12 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.commons.model.nip51Lists.relayFeeds +package com.vitorpamplona.amethyst.commons.model.nip51Lists.favoriteRelays import com.vitorpamplona.amethyst.commons.model.nip51Lists.relayLists.GenericRelayListCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.relayLists.FavoriteRelayListEvent -class RelayFeedsListDecryptionCache( +class FavoriteRelayListDecryptionCache( signer: NostrSigner, ) : GenericRelayListCache<FavoriteRelayListEvent>(signer) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/followSets/FollowSetDecryptionCache.kt similarity index 94% rename from commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/followSets/FollowSetDecryptionCache.kt index e31ca57983..b53e2a351b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/peopleList/PeopleListDecryptionCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/followSets/FollowSetDecryptionCache.kt @@ -18,18 +18,18 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList +package com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUserIdSet import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUsers import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUsersAndWords import com.vitorpamplona.quartz.nip51Lists.muteList.mutedWordSet import com.vitorpamplona.quartz.nip51Lists.muteList.mutedWords -import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent -class PeopleListDecryptionCache( +class FollowSetDecryptionCache( val signer: NostrSigner, ) { val cachedPrivateLists = PrivateTagArrayEventCache<FollowSetEvent>(signer) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/peopleList/PeopleList.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/followSets/PeopleList.kt similarity index 96% rename from commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/peopleList/PeopleList.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/followSets/PeopleList.kt index d0366e3dd9..8e412478c5 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/peopleList/PeopleList.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/followSets/PeopleList.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList +package com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.model.User diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/interestLists/InterestListDecryptionCache.kt similarity index 86% rename from commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/interestLists/InterestListDecryptionCache.kt index db80d69a1b..2fcd45b9c0 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip51Lists/interestLists/InterestListDecryptionCache.kt @@ -18,14 +18,14 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.commons.model.nip51Lists.hashtagLists +package com.vitorpamplona.amethyst.commons.model.nip51Lists.interestLists import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache -import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.hashtagSet +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent +import com.vitorpamplona.quartz.nip51Lists.interestList.hashtagSet -class HashtagListDecryptionCache( +class InterestListDecryptionCache( val signer: NostrSigner, ) { val cachedPrivateLists = PrivateTagArrayEventCache<InterestListEvent>(signer) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardDecryptionCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/UserAssertionDecryptionCache.kt similarity index 98% rename from commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardDecryptionCache.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/UserAssertionDecryptionCache.kt index 61bb8a3705..7fd54fb060 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardDecryptionCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/UserAssertionDecryptionCache.kt @@ -34,7 +34,7 @@ import com.vitorpamplona.quartz.nip85TrustedAssertions.users.summary * reading them requires the account's main key ([signer]); cards signed by any * other key never decrypt here. */ -class ContactCardDecryptionCache( +class UserAssertionDecryptionCache( val signer: NostrSigner, ) { val cachedPrivateCards = PrivateTagArrayEventCache<UserAssertionEvent>(signer, cacheSize = 100) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardsState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/UserAssertionsState.kt similarity index 98% rename from commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardsState.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/UserAssertionsState.kt index ce8635472b..1837e2334c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/ContactCardsState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip85TrustedAssertions/UserAssertionsState.kt @@ -51,10 +51,10 @@ import kotlinx.coroutines.flow.mapLatest * account itself ([signer]'s pubkey). */ @Stable -class ContactCardsState( +class UserAssertionsState( val signer: NostrSigner, val cache: ICacheProvider, - val decryptionCache: ContactCardDecryptionCache, + val decryptionCache: UserAssertionDecryptionCache, val emojiPacks: EmojiPackState, ) { private val accountUser: User? by lazy { cache.getOrCreateUser(signer.pubKey) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/topNavFeeds/FeedDecryptionCaches.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/topNavFeeds/FeedDecryptionCaches.kt index 2df60e89d5..ce85cb0878 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/topNavFeeds/FeedDecryptionCaches.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/topNavFeeds/FeedDecryptionCaches.kt @@ -20,16 +20,16 @@ */ package com.vitorpamplona.amethyst.commons.model.topNavFeeds +import com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets.FollowSetDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip51Lists.geohashLists.GeohashListDecryptionCache -import com.vitorpamplona.amethyst.commons.model.nip51Lists.hashtagLists.HashtagListDecryptionCache +import com.vitorpamplona.amethyst.commons.model.nip51Lists.interestLists.InterestListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip51Lists.muteList.MuteListDecryptionCache -import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip72Communities.CommunityListDecryptionCache class FeedDecryptionCaches( - val peopleListCache: PeopleListDecryptionCache, + val peopleListCache: FollowSetDecryptionCache, val muteListCache: MuteListDecryptionCache, val communityListCache: CommunityListDecryptionCache, - val hashtagCache: HashtagListDecryptionCache, + val hashtagCache: InterestListDecryptionCache, val geohashCache: GeohashListDecryptionCache, ) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/topNavFeeds/noteBased/NoteFeedFlow.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/topNavFeeds/noteBased/NoteFeedFlow.kt index d48e5704a0..bf9b8604aa 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/topNavFeeds/noteBased/NoteFeedFlow.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/topNavFeeds/noteBased/NoteFeedFlow.kt @@ -35,11 +35,11 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.Mute import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/moderation/notifications/NotificationKinds.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/moderation/notifications/NotificationKinds.kt index 1147d8eeaf..2c582e00c6 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/moderation/notifications/NotificationKinds.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/moderation/notifications/NotificationKinds.kt @@ -111,7 +111,7 @@ object NotificationKinds { myPubKeyHex: HexKey, isTargetAuthoredByMe: (targetNoteId: HexKey) -> Boolean = { false }, ): Boolean { - // Own events never notify — except zap receipts (LnZap/Nutzap/Onchain) + // Own events never notify — except zap receipts (ZapReceipt/Nutzap/Onchain) // which are signed by the LNURL provider or the payer, not by us. if (event.pubKey == myPubKeyHex && event !is ZapReceiptEvent && diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterBasicAccountInfoFromKeys.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterBasicAccountInfoFromKeys.kt index 6026a0bba8..e03dbe41b1 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterBasicAccountInfoFromKeys.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterBasicAccountInfoFromKeys.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterBookmarksAndReportsFromKey.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterBookmarksAndReportsFromKey.kt index 5d4a10c7a8..8b78c54e1c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterBookmarksAndReportsFromKey.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterBookmarksAndReportsFromKey.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.BookmarkSetEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterFollowsAndMutesFromKey.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterFollowsAndMutesFromKey.kt index fc0436f6a0..ce5bfa014e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterFollowsAndMutesFromKey.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/account/metadata/FilterFollowsAndMutesFromKey.kt @@ -28,9 +28,9 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListEvent import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/ContactCardFilters.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/ContactCardFilters.kt index 237c08b2f9..837719a7ef 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/ContactCardFilters.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/assemblers/ContactCardFilters.kt @@ -28,14 +28,14 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.dTag.DTag import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent -val ContactCardKindList = listOf(UserAssertionEvent.KIND) +val UserAssertionKindList = listOf(UserAssertionEvent.KIND) /** * Kind:30382 cards *about* [targets], written by [trustedAccounts] (the account * itself plus its WoT trust providers). Fetches nicknames and scores for the * users currently on screen. */ -fun filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay( +fun filterUserAssertionsToTargetKeysFromTrustedAccountsInTheRelay( targets: Set<HexKey>, trustedAccounts: List<HexKey>, relay: NormalizedRelayUrl, @@ -49,7 +49,7 @@ fun filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay( ExplainedFilter( purpose = SubPurpose.PROFILE_METADATA, accountPubKeys = listOfNotNull(accountPubKey), - kinds = ContactCardKindList, + kinds = UserAssertionKindList, authors = trustedAccounts, // kind:30382 addresses the target user in the d-tag tags = mapOf(DTag.TAG_NAME to targets.sorted()), @@ -68,7 +68,7 @@ fun filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay( * "Observing Profiles" — explained as *"profiles of the people currently on screen"* — made every * logged-in account look like it was watching somebody. */ -fun filterContactCardsByAuthorInTheRelay( +fun filterUserAssertionsByAuthorInTheRelay( relay: NormalizedRelayUrl, authors: List<HexKey>, since: Long?, @@ -81,7 +81,7 @@ fun filterContactCardsByAuthorInTheRelay( purpose = SubPurpose.ACCOUNT_DATA, // This variant fetches the accounts' OWN contact cards, so the authors are the owners. accountPubKeys = authors, - kinds = ContactCardKindList, + kinds = UserAssertionKindList, authors = authors, limit = limit, since = since, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByAllCommunities.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByAllCommunities.kt index 714813a311..6492b4e3ee 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByAllCommunities.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByAllCommunities.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent fun filterFollowSetsAllCommunities( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByAuthors.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByAuthors.kt index bcb5f3d944..86ea1eb063 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByAuthors.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByAuthors.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent fun filterFollowSetsAuthors( relay: NormalizedRelayUrl, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByCommunity.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByCommunity.kt index 65483bc063..b1589dc89b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByCommunity.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByCommunity.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent fun filterFollowSetsByCommunity( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByGeohash.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByGeohash.kt index 8973d4358d..9b51e44710 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByGeohash.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByGeohash.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent fun filterFollowSetsByGeohash( relay: NormalizedRelayUrl, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByHashtag.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByHashtag.kt index 21749a61e7..b228a62731 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByHashtag.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsByHashtag.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent fun filterFollowSetsByHashtag( relay: NormalizedRelayUrl, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsGlobal.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsGlobal.kt index c62c7bf76e..b6b59bb3c4 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsGlobal.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/discover/nip51FollowSets/FilterFollowSetsGlobal.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFil import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent fun filterFollowSetsGlobal( relays: GlobalTopNavPerRelayFilterSet, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfileLists.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfileLists.kt index 1ebb50c63b..444f4e1695 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfileLists.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/profile/FilterUserProfileLists.kt @@ -29,9 +29,9 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent val UserProfileListKinds = diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/UserCardsSubAssembler.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/UserCardsSubAssembler.kt index 9c4ffbc31e..bee7470979 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/UserCardsSubAssembler.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/user/watchers/UserCardsSubAssembler.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.commons.relayClient.user.watchers import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.amethyst.commons.model.toHexSet -import com.vitorpamplona.amethyst.commons.relayClient.assemblers.filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay +import com.vitorpamplona.amethyst.commons.relayClient.assemblers.filterUserAssertionsToTargetKeysFromTrustedAccountsInTheRelay import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.commons.relayClient.user.UserFinderQueryState import com.vitorpamplona.amethyst.commons.relays.MutableTime @@ -103,14 +103,14 @@ class UserCardsSubAssembler( val groups = groupByRelayPresence(lastUsersOnFilter, relay) val trustedAccounts = trustedUsersInThisRelay.sorted() listOfNotNull( - filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay( + filterUserAssertionsToTargetKeysFromTrustedAccountsInTheRelay( accountPubKey = soleAccountPubKey, targets = groups.usersWithoutEose.toHexSet(), trustedAccounts = trustedAccounts, relay = relay, since = null, ), - filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay( + filterUserAssertionsToTargetKeysFromTrustedAccountsInTheRelay( accountPubKey = soleAccountPubKey, targets = groups.usersWithEose.toHexSet(), trustedAccounts = trustedAccounts, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/KindRegistry.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/KindRegistry.kt index 34b7ce090f..fb790f0858 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/KindRegistry.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/KindRegistry.kt @@ -32,7 +32,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/RenderableKinds.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/RenderableKinds.kt index 214e789056..71636dfc37 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/RenderableKinds.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/RenderableKinds.kt @@ -47,8 +47,8 @@ import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip51Lists.videoCurationSet.VideoCurationSetEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/ChatPreviewTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/ChatPreviewTest.kt index d050ab8e93..b831f2d452 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/ChatPreviewTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/ChatPreviewTest.kt @@ -37,7 +37,7 @@ class ChatPreviewTest { private val ciphertext = "0tyoSVovKSK9uDKLUVMs137TD0b+vz+fjoJN+gG3Bk4=?iv=6r6yPQ==" - private fun privateDm( + private fun encryptedDm( author: String = other, recipient: String = me, ) = EncryptedDmEvent( @@ -63,7 +63,7 @@ class ChatPreviewTest { @Test fun nip04DmCarriesCiphertext() { - assertTrue(privateDm().hasEncryptedContent()) + assertTrue(encryptedDm().hasEncryptedContent()) } @Test @@ -129,14 +129,14 @@ class ChatPreviewTest { fun decryptedDmRendersThePlaintext() { assertEquals( ChatPreview.Body("hi!"), - chatPreviewOf(privateDm(), decrypted = "hi!", myPubKey = me, canDecrypt = true), + chatPreviewOf(encryptedDm(), decrypted = "hi!", myPubKey = me, canDecrypt = true), ) } /** The regression under test: the raw base64 blob must never become the preview. */ @Test fun pendingDmNeverFallsThroughToCiphertext() { - val preview = chatPreviewOf(privateDm(), decrypted = null, myPubKey = me, canDecrypt = true) + val preview = chatPreviewOf(encryptedDm(), decrypted = null, myPubKey = me, canDecrypt = true) assertEquals(ChatPreview.Decrypting, preview) assertFalse(preview is ChatPreview.Body) @@ -146,7 +146,7 @@ class ChatPreviewTest { fun readOnlyAccountCannotEverDecrypt() { assertEquals( ChatPreview.Undecryptable, - chatPreviewOf(privateDm(), decrypted = null, myPubKey = me, canDecrypt = false), + chatPreviewOf(encryptedDm(), decrypted = null, myPubKey = me, canDecrypt = false), ) } @@ -155,7 +155,7 @@ class ChatPreviewTest { assertEquals( ChatPreview.Undecryptable, chatPreviewOf( - privateDm(author = other, recipient = stranger), + encryptedDm(author = other, recipient = stranger), decrypted = null, myPubKey = me, canDecrypt = true, @@ -168,7 +168,7 @@ class ChatPreviewTest { assertEquals( ChatPreview.Decrypting, chatPreviewOf( - privateDm(author = me, recipient = other), + encryptedDm(author = me, recipient = other), decrypted = null, myPubKey = me, canDecrypt = true, diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/moderation/notifications/NotificationKindsTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/moderation/notifications/NotificationKindsTest.kt index f9aee77c88..e472f7c54e 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/moderation/notifications/NotificationKindsTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/moderation/notifications/NotificationKindsTest.kt @@ -139,7 +139,7 @@ class NotificationKindsTest { sig = someSig, ) - private fun privateDm( + private fun encryptedDm( author: String, pTag: String = me, ): EncryptedDmEvent = @@ -266,9 +266,9 @@ class NotificationKindsTest { @Test fun myOwnZapReceiptStillAccepted() { - // LnZap receipts are signed by the LNURL provider, not by us, + // ZapReceipt receipts are signed by the LNURL provider, not by us, // so `pubKey == me` never applies — but even if it did the - // helper explicitly allows LnZap/Nutzap/OnchainZap own-events. + // helper explicitly allows ZapReceipt/Nutzap/OnchainZap own-events. assertTrue(acceptsFor(zapReceipt(pTag = me))) } @@ -280,8 +280,8 @@ class NotificationKindsTest { // DMs -------------------------------------------------------------------- @Test - fun privateDmAccepted() { - assertTrue(acceptsFor(privateDm(author = alice))) + fun encryptedDmAccepted() { + assertTrue(acceptsFor(encryptedDm(author = alice))) } @Test diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletBrokerTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletBrokerTest.kt index aa5d930cff..586a158653 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletBrokerTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletBrokerTest.kt @@ -42,7 +42,7 @@ import com.vitorpamplona.quartz.nip01Core.crypto.verify import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.async @@ -137,7 +137,7 @@ class NappletBrokerTest { fromPublicKey: HexKey, ): String = throw NotImplementedError() - override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = throw NotImplementedError() + override suspend fun decryptZapEvent(event: ZapRequestEvent): PrivateZapEvent = throw NotImplementedError() override suspend fun deriveKey(nonce: HexKey): HexKey = throw NotImplementedError() diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/onchain/OnchainZapSenderTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/onchain/OnchainZapSenderTest.kt index d0f61a9ad3..f8937547c1 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/onchain/OnchainZapSenderTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/onchain/OnchainZapSenderTest.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nipBCOnchainZaps.builder.OnchainZapBuilder import com.vitorpamplona.quartz.nipBCOnchainZaps.chain.BitcoinAddressTx @@ -228,7 +228,7 @@ class OnchainZapSenderTest { fromPublicKey: HexKey, ) = inner.nip44Decrypt(ciphertext, fromPublicKey) - override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = inner.decryptZapEvent(event) + override suspend fun decryptZapEvent(event: ZapRequestEvent): PrivateZapEvent = inner.decryptZapEvent(event) override suspend fun deriveKey(nonce: HexKey): HexKey = inner.deriveKey(nonce) diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModelTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModelTest.kt index 5c27c8c828..afb4eba786 100644 --- a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModelTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModelTest.kt @@ -35,7 +35,7 @@ import com.vitorpamplona.nestsclient.transport.WebTransportSession import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -589,7 +589,7 @@ class NestViewModelTest { fromPublicKey: String, ): String = error("not used") - override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = error("not used") + override suspend fun decryptZapEvent(event: ZapRequestEvent): PrivateZapEvent = error("not used") override suspend fun deriveKey(nonce: String): String = error("not used") diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip51Lists/ui/BookmarkGroupItem.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip51Lists/ui/BookmarkGroupItem.kt index 6d2ea0334b..cb27ce2c4a 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip51Lists/ui/BookmarkGroupItem.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip51Lists/ui/BookmarkGroupItem.kt @@ -50,7 +50,7 @@ import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.nip51Lists.BookmarkType -import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList +import com.vitorpamplona.amethyst.commons.model.nip51Lists.bookmarkSets.BookmarkSet import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.bookmark_list_articles_btn_label import com.vitorpamplona.amethyst.commons.resources.bookmark_list_clone_btn_label @@ -85,7 +85,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer @Composable fun BookmarkGroupItem( modifier: Modifier = Modifier, - bookmarkList: LabeledBookmarkList, + bookmarkList: BookmarkSet, onClick: (bookmarkItemType: BookmarkType) -> Unit, onRename: () -> Unit, onDescriptionChange: () -> Unit, diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip51Lists/ui/ListOfBookmarkGroupsFeedView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip51Lists/ui/ListOfBookmarkGroupsFeedView.kt index fe891a2366..d208a1694f 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip51Lists/ui/ListOfBookmarkGroupsFeedView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip51Lists/ui/ListOfBookmarkGroupsFeedView.kt @@ -46,7 +46,7 @@ import com.vitorpamplona.amethyst.commons.model.nip51Lists.BookmarkType import com.vitorpamplona.amethyst.commons.model.nip51Lists.GitRepositoryListState import com.vitorpamplona.amethyst.commons.model.nip51Lists.OldBookmarkListState import com.vitorpamplona.amethyst.commons.model.nip51Lists.PinListState -import com.vitorpamplona.amethyst.commons.model.nip51Lists.labeledBookmarkLists.LabeledBookmarkList +import com.vitorpamplona.amethyst.commons.model.nip51Lists.bookmarkSets.BookmarkSet import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.bookmark_list_icon_label import com.vitorpamplona.amethyst.commons.resources.bookmarks_explainer @@ -73,17 +73,17 @@ fun ListOfBookmarkGroupsFeedView( oldBookmarks: OldBookmarkListState, pinnedNotes: PinListState, repositories: GitRepositoryListState, - groupListFeedSource: StateFlow<List<LabeledBookmarkList>>, + groupListFeedSource: StateFlow<List<BookmarkSet>>, openDefaultBookmarks: () -> Unit, openOldBookmarks: () -> Unit, openPinnedNotes: () -> Unit, openRepositories: () -> Unit, openPodcasts: () -> Unit, onOpenItem: (String, BookmarkType) -> Unit, - onRenameItem: (targetBookmarkGroup: LabeledBookmarkList) -> Unit, - onItemDescriptionChange: (bookmarkGroup: LabeledBookmarkList) -> Unit, - onItemClone: (bookmarkGroup: LabeledBookmarkList, customName: String?, customDesc: String?) -> Unit, - onDeleteItem: (bookmarkGroup: LabeledBookmarkList) -> Unit, + onRenameItem: (targetBookmarkGroup: BookmarkSet) -> Unit, + onItemDescriptionChange: (bookmarkGroup: BookmarkSet) -> Unit, + onItemClone: (bookmarkGroup: BookmarkSet, customName: String?, customDesc: String?) -> Unit, + onDeleteItem: (bookmarkGroup: BookmarkSet) -> Unit, listState: LazyListState = rememberLazyListState(), ) { val bookmarkGroupFeedState by groupListFeedSource.collectAsStateWithLifecycle() @@ -119,7 +119,7 @@ fun ListOfBookmarkGroupsFeedView( itemsIndexed( bookmarkGroupFeedState, - key = { _: Int, item: LabeledBookmarkList -> item.identifier }, + key = { _: Int, item: BookmarkSet -> item.identifier }, ) { _, groupItem -> BookmarkGroupItem( modifier = Modifier.fillMaxSize().animateItem(), diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip51Lists/ui/PeopleListItem.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip51Lists/ui/PeopleListItem.kt index 2cee4e8d6a..c73b4f9c9a 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip51Lists/ui/PeopleListItem.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip51Lists/ui/PeopleListItem.kt @@ -51,7 +51,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache -import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleList +import com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets.PeopleList import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.cancel import com.vitorpamplona.amethyst.commons.resources.follow_set_copy_action_btn_label diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip85TrustedAssertions/ui/EditNicknameDialog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip85TrustedAssertions/ui/EditNicknameDialog.kt index b24c98cbb5..400f75d209 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip85TrustedAssertions/ui/EditNicknameDialog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip85TrustedAssertions/ui/EditNicknameDialog.kt @@ -41,7 +41,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState -import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.ContactCardsState +import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.UserAssertionsState import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.nickname_cancel @@ -62,31 +62,31 @@ import org.jetbrains.compose.resources.stringResource * Typing `:` offers the account's NIP-30 custom emojis; the mappings for any * shortcode used are embedded (also encrypted) so the nickname renders with them. * - * Shared by every front end: the caller supplies the account's [contactCards] + * Shared by every front end: the caller supplies the account's [userAssertions] * and publishes the result in [onSave] (e.g. through its outbox relays). On * Android, compose `WatchAndLoadMyEmojiList` alongside so the emoji packs load. */ @Composable fun EditNicknameDialog( user: User, - contactCards: ContactCardsState, + userAssertions: UserAssertionsState, onSave: (petName: String?, summary: String?) -> Unit, onDismiss: () -> Unit, ) { val nickname = rememberTextFieldState() val summary = rememberTextFieldState() - val emojiSuggestions = remember(contactCards) { EmojiSuggestionState(contactCards.emojiPacks) } + val emojiSuggestions = remember(userAssertions) { EmojiSuggestionState(userAssertions.emojiPacks) } // which field the emoji autocomplete should insert into: the last one edited val emojiTarget = remember { mutableStateOf<TextFieldState?>(null) } // Prefill with the card's current encrypted values, if any. Decryption can // be slow on external signers, so don't clobber anything already typed. LaunchedEffect(user) { - contactCards + userAssertions .petName(user.pubkeyHex) ?.takeIf { nickname.text.isEmpty() } ?.let { nickname.setTextAndPlaceCursorAtEnd(it) } - contactCards + userAssertions .summary(user.pubkeyHex) ?.takeIf { summary.text.isEmpty() } ?.let { summary.setTextAndPlaceCursorAtEnd(it) } diff --git a/desktopApp/plans/2026-05-12-feat-desktop-wallet-zapping-test-coverage-plan.md b/desktopApp/plans/2026-05-12-feat-desktop-wallet-zapping-test-coverage-plan.md index 13e2a9b82d..d3a4161b94 100644 --- a/desktopApp/plans/2026-05-12-feat-desktop-wallet-zapping-test-coverage-plan.md +++ b/desktopApp/plans/2026-05-12-feat-desktop-wallet-zapping-test-coverage-plan.md @@ -17,7 +17,7 @@ origin: desktopApp/plans/2026-05-12-wallet-zapping-phase1-2-plan.md 1. **mockk callback capture**: Use `slot<(Event, NormalizedRelayUrl) -> Unit>()` + `capture(slot)` to intercept relay subscription callbacks and feed simulated wallet responses 2. **GlobalScope.launch problem**: NwcPaymentHandler uses `GlobalScope.launch(Dispatchers.IO)` inside callbacks — tests need real time via `withTimeout(5.seconds)` to await (or refactor to inject scope) -3. **LnZapPaymentResponseEvent.createResponse()** exists — build real encrypted test responses with deterministic keys, no need to mock encryption +3. **NwcResponseEvent.createResponse()** exists — build real encrypted test responses with deterministic keys, no need to mock encryption 4. **runTest auto-advances virtual time** — timeout tests resolve instantly without real delays 5. **Codebase prefers anonymous objects** over mockk for complex interfaces (see RelayConnectionManagerTest) @@ -35,7 +35,7 @@ methods, wallet column state management, and zap dialog logic. | NIP-47 URI parsing | quartz | `Nip47WalletConnectTest.kt` | Complete | | NWC request serialization | quartz | `RequestTest.kt` (50+ cases) | Complete | | NWC response parsing | quartz | `ResponseTest.kt` (50+ cases) | Complete | -| NWC event encryption | quartz | `LnZapPaymentRequestEventTest.kt` | Complete | +| NWC event encryption | quartz | `NwcRequestEventTest.kt` | Complete | | Alby interop | quartz | `AlbyInteropTest.kt` (60+ cases) | Complete | | Account management | desktopApp | `AccountManager*Test.kt` (5 files) | Complete | | Cache pipeline | desktopApp | `DesktopCachePipelineTest.kt` | Complete | @@ -187,9 +187,9 @@ class NwcPaymentHandlerTest { **Challenge:** The `subscribeOnRelay` callback runs asynchronously. Tests must capture the `onEvent` lambda and invoke it with a simulated -`LnZapPaymentResponseEvent`. This requires building encrypted response +`NwcResponseEvent`. This requires building encrypted response events using the wallet's signer — similar to -`quartz/nip47WalletConnect/LnZapPaymentRequestEventTest.kt`. +`quartz/nip47WalletConnect/NwcRequestEventTest.kt`. **Alternative (simpler):** If capturing the relay callback is too complex, extract the response processing logic into a testable pure function and @@ -349,7 +349,7 @@ class NwcRpcIntegrationTest { val requestEvent = client.getBalance() // 2. Verify request is properly encrypted - assertEquals(LnZapPaymentRequestEvent.KIND, requestEvent.kind) + assertEquals(NwcRequestEvent.KIND, requestEvent.kind) assertTrue(requestEvent.content.isNotBlank()) // 3. Wallet decrypts and verifies method @@ -357,7 +357,7 @@ class NwcRpcIntegrationTest { assertIs<GetBalanceMethod>(decryptedRequest) // 4. Wallet builds encrypted response - val responseEvent = LnZapPaymentResponseEvent.create( + val responseEvent = NwcResponseEvent.create( request = requestEvent, response = GetBalanceSuccessResponse( result = GetBalanceSuccessResponse.GetBalanceResult(balance = 125000), @@ -429,7 +429,7 @@ extraction needed). | Mocking relay callback is complex | Medium | Extract response processing into pure function, test that directly | | Compose state testing without Compose | Medium | Extract state holder class (Option A) | | `formatSats` is private | Low | Change to `internal` visibility | -| Encrypted response building may need quartz test utils | Low | Use same pattern as `LnZapPaymentRequestEventTest.kt` | +| Encrypted response building may need quartz test utils | Low | Use same pattern as `NwcRequestEventTest.kt` | ## Sources @@ -437,4 +437,4 @@ extraction needed). - NIP-47 protocol tests: `quartz/src/commonTest/.../nip47WalletConnect/` (13 files) - Test framework: kotlin.test + mockk 1.14.9 + kotlinx-coroutines-test 1.10.2 - Mock pattern: `AccountManagerBunkerLoginTest.kt` (relaxed mockk + coEvery) -- Crypto test pattern: `LnZapPaymentRequestEventTest.kt` (deterministic KeyPair) +- Crypto test pattern: `NwcRequestEventTest.kt` (deterministic KeyPair) diff --git a/desktopApp/plans/2026-07-23-feat-desktop-moderation-safety-plan.md b/desktopApp/plans/2026-07-23-feat-desktop-moderation-safety-plan.md index 62f1e5fc80..a2fc90ef6a 100644 --- a/desktopApp/plans/2026-07-23-feat-desktop-moderation-safety-plan.md +++ b/desktopApp/plans/2026-07-23-feat-desktop-moderation-safety-plan.md @@ -30,7 +30,7 @@ cruxes**, de-risking the whole feature: → Crux 2 resolved: reuse `invalidateData()`, don't invent a bus. 3. **Concrete APIs confirmed** (see Research Insights under Technical Approach): `MuteListEvent.create/add/remove(MuteTag, isPrivate, signer)` where `MuteTag ∈ - {UserTag, HashtagTag(word), EventTag(thread)}`; `PeopleListEvent.addUser/ + {UserTag, HashtagTag(word), EventTag(thread)}`; `FollowSetEvent.addUser/ removeUser` for blocks; publish via `signer.sign(template)` + `relayManager.broadcastToAll(signed)` (precedent `reactToNote`/`repostNote` in `desktopApp/.../ui/NoteActions.kt`). @@ -85,7 +85,7 @@ enforcement, reusing quartz protocol and the existing spam-collapse reveal: `isAcceptable`, so DMs get filtered for free once the predicate is real. - **Actions:** `hideUser`/`showUser`/`blockUser`/`hideWord`/`hideThread` on `DesktopIAccount` build → sign → publish an updated `MuteListEvent`/ - `PeopleListEvent`. + `FollowSetEvent`. - **Report:** `report(note|user, type, comment)` reuses quartz `ReportEvent.build` (kind 1984) + `ReportType`, published to the account's public + private outbox (match Android). New `ReportNoteDialog` ported to desktop Compose, wired into @@ -103,7 +103,7 @@ enforcement, reusing quartz protocol and the existing spam-collapse reveal: flowchart TD subgraph relays[Relays] K10000[kind 10000 MuteListEvent - encrypted] - K10001[kind 10001 PeopleListEvent - block] + K10001[kind 10001 FollowSetEvent - block] K1068[content notes, some content-warning tagged] end K10000 & K10001 --> SUB[Main.kt account subscription] @@ -130,7 +130,7 @@ flowchart TD | Report types | `ReportType` enum (SPAM/PROFANITY/IMPERSONATION/NUDITY/ILLEGAL/MALWARE/VIOLENCE/…) | `quartz/.../nip56Reports/ReportType.kt` | | CW detection | `Event.isSensitiveOrNSFW()`, `contentWarningReason()` | `quartz/.../nip36SensitiveContent/EventExt.kt` | | Hidden check | `Note.isHiddenFor(liveHiddenUsers)` (users+words+threads) | `quartz/.../Note.kt` | -| Mute/block events | `MuteListEvent` (10000), `PeopleListEvent` (10001) | `quartz/.../nip51Lists/…` | +| Mute/block events | `MuteListEvent` (10000), `FollowSetEvent` (10001) | `quartz/.../nip51Lists/…` | | Reveal primitive | `SpamCheckedNoteRender` `forceReveal` + `rememberSaveable(id)` | `desktopApp/.../ui/note/SpamCheckedNoteRender.kt` | ### Android references to port @@ -183,7 +183,7 @@ MuteListEvent.remove(earlierVersion, mute, signer) // reading private entries requires the signer: earlierVersion.privateTags(signer) // throws SignerExceptions.UnauthorizedDecryptionException on read-only // block (kind 10001) -PeopleListEvent.addUser(...) / removeUser(...) +FollowSetEvent.addUser(...) / removeUser(...) ``` MuteTag subtypes: `UserTag` (pubkey), `HashtagTag` (word), `EventTag` (thread root). Enforcement then reads these via `Note.isHiddenFor(liveHiddenUsers)`. diff --git a/desktopApp/plans/2026-07-27-feat-desktop-profile-actions-plan.md b/desktopApp/plans/2026-07-27-feat-desktop-profile-actions-plan.md index 5cdbf395ce..11e777743e 100644 --- a/desktopApp/plans/2026-07-27-feat-desktop-profile-actions-plan.md +++ b/desktopApp/plans/2026-07-27-feat-desktop-profile-actions-plan.md @@ -44,13 +44,13 @@ Open Desktop Messages for this pubkey via the existing deck-column pattern. (`ShareMenu.kt:56`) shape. No auth required; own + others. ## Action 3 — Add to follow-pack (kind 39089) — mostly REUSE -- **Picker source:** `FollowPacksState.allPacks: StateFlow<List<FollowListEvent>>` +- **Picker source:** `FollowPacksState.allPacks: StateFlow<List<StarterPackEvent>>` (`followpacks/FollowPacksState.kt:84`) — zero-packs returns `emptyList()`. -- **Add member:** `FollowListEvent.add(earlierVersion, person = UserTag(pubKey, relayHint), signer)` - (`quartz nip51Lists/followList/FollowListEvent.kt:116`; `UserTag` from `muteList/tags/UserTag.kt:35`). +- **Add member:** `StarterPackEvent.add(earlierVersion, person = UserTag(pubKey, relayHint), signer)` + (`quartz nip51Lists/starterPack/StarterPackEvent.kt:116`; `UserTag` from `muteList/tags/UserTag.kt:35`). Publish via `relayManager.broadcastToAll(event)` + `cache.consume(event, relay, wasVerified=false)` (pattern in `FollowPackDetailScreen.kt:178-192`). -- **Thin `commons/actions/FollowPackActions.kt`** = pure builder wrapping `FollowListEvent.add` +- **Thin `commons/actions/FollowPackActions.kt`** = pure builder wrapping `StarterPackEvent.add` (returns the signed event; desktop layer publishes). No hand-rolled tag assembly. - **Zero-packs → offer "Create new pack"** (reuse `FollowPackEditor`); no dead-end modal. - **Stale-list guard:** disable until the target pack's current 39089 has loaded (`add` onto a stale @@ -58,33 +58,33 @@ Open Desktop Messages for this pubkey via the existing deck-column pattern. - **Modal caveat:** the picker must NOT use `rememberSubscription` (broken in AlertDialog); the `allPacks` StateFlow is already hoisted, so read it directly. -## Action 4 — Block (`PeopleListEvent` kind 30000, d=`mute`) +## Action 4 — Block (`FollowSetEvent` kind 30000, d=`mute`) **Much of this already exists on moderation-safety — Block is mostly wiring, mirroring mute.** > **Revision of parent's "extract to commons" default:** moderation-safety keeps the *mute* write > **inline** on `DesktopIAccount.updateMuteList` (not a commons builder). To avoid the architecture > reviewer's "two parallel patterns" smell, Block should be **inline and symmetric with mute** — NOT -> a `commons/actions/BlockActions.kt`. The quartz `PeopleListEvent.addUser` IS the shared builder; a +> a `commons/actions/BlockActions.kt`. The quartz `FollowSetEvent.addUser` IS the shared builder; a > commons wrapper adds nothing. (If a commons home is later wanted, extract mute + block *together*.) **Already present (REUSE, no changes):** -- `PeopleListEvent` (quartz `nip51Lists/peopleList/PeopleListEvent.kt`): `KIND=30000`, +- `FollowSetEvent` (quartz `nip51Lists/followSet/FollowSetEvent.kt`): `KIND=30000`, `BLOCK_LIST_D_TAG="mute"`, `createBlockAddress(pubKey)`; `addUser(earlierVersion, pubKeyHex, relayHint, isPrivate, signer)`, `removeUser(earlierVersion, pubKeyHex, isUserPrivate, signer)`, `remove/removeAll`. -- `PeopleListDecryptionCache(signer)` (commons) — `userIdSet(event)` merges public+private, +- `FollowSetDecryptionCache(signer)` (commons) — `userIdSet(event)` merges public+private, **fail-closed** (`privateTags` → null / `UnauthorizedDecryptionException` on read-only). - `DesktopHiddenUsersState` **already loads the own kind-30000 block list** - (`blockListNote = cache.getOrCreateAddressableNote(PeopleListEvent.createBlockAddress(signer.pubKey))`). + (`blockListNote = cache.getOrCreateAddressableNote(FollowSetEvent.createBlockAddress(signer.pubKey))`). - `publishAndConfirmDetailed(event, relays, timeout): Map<relay,Boolean>` (quartz `NostrClientPublishExt.kt`); `DmSendTracker.sendBatch` shows the confirmed-publish + status pattern. **WIRE (new, small):** -- `DesktopHiddenUsersState.currentBlockList(): PeopleListEvent?` — mirror the existing - `currentMuteList()` (`blockListNote.event as? PeopleListEvent`). +- `DesktopHiddenUsersState.currentBlockList(): FollowSetEvent?` — mirror the existing + `currentMuteList()` (`blockListNote.event as? FollowSetEvent`). - `DesktopIAccount.blockUser(pubkeyHex)` / `unblockUser(pubkeyHex)` + private `updateBlockList(tag, isPrivate, add)` — **mirror `updateMuteList` exactly**: load - `currentBlockList()`, `PeopleListEvent.addUser/removeUser` (or `create` **only if null**), + `currentBlockList()`, `FollowSetEvent.addUser/removeUser` (or `create` **only if null**), `localCache.justConsumeMyOwnEvent(event)` (optimistic), then publish. - Publish Block via **`publishAndConfirmDetailed`** (Block is security-relevant; Desktop NIP-42 AUTH is partial so fire-and-forget `broadcastToAll` — what `publishModeration` uses for mute — risks @@ -106,7 +106,7 @@ moderation-safety already merges mute ∪ block: `DesktopHiddenUsersState.assemb ## Acceptance criteria - [ ] DM opens the correct 1:1 room (`ChatroomKey(setOf(pubkey))` + `selectRoom`); graceful when target has no inbox relays / read-only. - [ ] Share yields a valid `npub` / `nostr:` link via `toNpub()`/`toNProfile()` + `copyToClipboard`. -- [ ] Add-to-list adds to a chosen pack (39089, `FollowListEvent.add`) and persists; zero-packs offers create-new; disabled until pack loaded. +- [ ] Add-to-list adds to a chosen pack (39089, `StarterPackEvent.add`) and persists; zero-packs offers create-new; disabled until pack loaded. - [ ] Block (30000, d=`mute`) hides user across feeds/threads/replies/profile (via existing `LiveHiddenUsers`); Unblock reverses. - [ ] Block round-trip preserves prior private entries (test mirrors `MuteListEventTest.add_eventTagPreservesPriorUserAndWordTags`); disabled until `currentBlockList()` loaded; decrypt-fail is fail-closed. - [ ] Block publish confirmed via `publishAndConfirmDetailed` (not fire-and-forget); optimistic UI rolls back on total failure. @@ -117,7 +117,7 @@ moderation-safety already merges mute ∪ block: `DesktopHiddenUsersState.assemb ## Open questions 1. **Block write location:** inline on `DesktopIAccount` symmetric with mute (recommended, overrides parent's "extract to commons") — confirm, or still want a `commons/actions` home (then extract mute+block together)? 2. Fold Block into a combined "block & mute" (Android-style dialog) or keep standalone? (parent Open Q #1) — note enforcement effect is identical since `LiveHiddenUsers` already unions both. -3. Add-member to a 39089 pack: `FollowListEvent.add` inline in `FollowPackActions` builder — any existing desktop add-member path to prefer? (agent found none.) +3. Add-member to a 39089 pack: `StarterPackEvent.add` inline in `FollowPackActions` builder — any existing desktop add-member path to prefer? (agent found none.) ### Resolved by research - ✅ moderation-safety already loads the own kind-30000 list and unions mute ∪ block in `LiveHiddenUsers` — **enforcement needs no new code** (retires parent Open Q #3 / #6). @@ -127,9 +127,9 @@ moderation-safety already merges mute ∪ block: `DesktopHiddenUsersState.assemb - Parent plan. Branch `origin/feat/desktop-moderation-safety`: `DesktopIAccount.kt` (`updateMuteList`, `hideUser/showUser`, `publishModeration`, header menu ~L350-390), `DesktopHiddenUsersState.kt` (`currentMuteList`, loads block via `createBlockAddress`, `assemble()`). -- quartz `nip51Lists/peopleList/PeopleListEvent.kt` (KIND 30000, d=`mute`, `addUser/removeUser`), - commons `PeopleListDecryptionCache.kt`, commons `IAccount.kt` (`LiveHiddenUsers.isUserHidden`), +- quartz `nip51Lists/followSet/FollowSetEvent.kt` (KIND 30000, d=`mute`, `addUser/removeUser`), + commons `FollowSetDecryptionCache.kt`, commons `IAccount.kt` (`LiveHiddenUsers.isUserHidden`), quartz `NostrClientPublishExt.kt` (`publishAndConfirm`/`Detailed`), `DmSendTracker.kt`. -- Packs/DM/share: `followpacks/FollowPacksState.kt` (`allPacks`), quartz `FollowListEvent.kt` +- Packs/DM/share: `followpacks/FollowPacksState.kt` (`allPacks`), quartz `StarterPackEvent.kt` (`add`), `ChatroomKey.kt`, `ui/chats/ChatroomListState.kt` (`selectRoom`, `fetchMetadataIfNeeded`), `ShareMenu.kt` (`copyToClipboard`), `nip19Bech32/ByteArrayExt.kt` (`toNpub`/`toNProfile`). diff --git a/desktopApp/plans/2026-07-27-feat-desktop-profile-parity-plan.md b/desktopApp/plans/2026-07-27-feat-desktop-profile-parity-plan.md index cecd64ede7..f6b948a473 100644 --- a/desktopApp/plans/2026-07-27-feat-desktop-profile-parity-plan.md +++ b/desktopApp/plans/2026-07-27-feat-desktop-profile-parity-plan.md @@ -20,11 +20,11 @@ Android (`amethyst/`), for both viewing and editing. Six parallel research/review agents (codebase-grounding, architecture, security, simplicity, flow-gaps, past-learnings) verified this plan against source. Key changes folded in: -1. **CORRECTION — Block list kind was wrong.** Block is **`PeopleListEvent` kind 30000, - d-tag `"mute"`** — *not* 30382. Kind 30382 is already **NIP-85 `ContactCardEvent` +1. **CORRECTION — Block list kind was wrong.** Block is **`FollowSetEvent` kind 30000, + d-tag `"mute"`** — *not* 30382. Kind 30382 is already **NIP-85 `UserAssertionEvent` (WoT/GrapeRank)** in this codebase; using it would collide with the WoT feature. -2. **Don't hand-roll Block.** `PeopleListEvent.addUser(earlierVersion, isPrivate=true)` + - `BlockPeopleListState` + `PeopleListDecryptionCache` already exist and do a correct, +2. **Don't hand-roll Block.** `FollowSetEvent.addUser(earlierVersion, isPrivate=true)` + + `BlockPeopleListState` + `FollowSetDecryptionCache` already exist and do a correct, fail-closed decrypt→merge→encrypt round-trip. The new commons builder is a **thin adapter**; **never call `create()` on an already-existing (replaceable) list** — that silently un-blocks everyone. @@ -54,7 +54,7 @@ Phases 2 and 3 are unpacked in dedicated grounded sub-plans (Phase 1/4 need no f enforcement code, no commons extraction** (retires Open Q #3 & #6, and the "unify enforcement now" task). 2. **Block is inline, not a commons builder.** moderation-safety keeps *mute* inline on `DesktopIAccount.updateMuteList`; Block mirrors it (`blockUser/unblockUser/updateBlockList`) using - quartz `PeopleListEvent.addUser/removeUser`. This overrides the "extract Block to `commons/actions/`" + quartz `FollowSetEvent.addUser/removeUser`. This overrides the "extract Block to `commons/actions/`" line below and resolves the architecture reviewer's two-parallel-patterns smell. (`FollowPackActions` may still be a thin commons builder.) 3. **`observeEvents` is a fallback, not a requirement.** Existing Desktop tabs use a cache-scan @@ -69,7 +69,7 @@ Phases 2 and 3 are unpacked in dedicated grounded sub-plans (Phase 1/4 need no f | mute/block/report missing on Desktop | mute + report built on **unmerged** `feat/desktop-moderation-safety` (`DesktopIAccount.hideUser/showUser/report`, desktop `ReportNoteDialog`, header overflow menu, `DesktopHiddenUsersState`, feed enforcement, NIP-36 blur). **Not in this worktree** (currently on `main`). | Build on top; do NOT re-implement mute/report. **Resolve the base first** (see Base Strategy). | | Extract mute/block/report to `commons` | moderation-safety put mute/report **desktop-local** on `DesktopIAccount` | New builders (Block, Add-to-pack) → `commons/actions/` (matches `FollowActions`/`ReportAction`). Route Block's **write** through `DesktopIAccount` symmetric with `updateMuteList`. Unify **enforcement** now; extract shared `HiddenUsersState` as follow-up. | | Rich-text bio needs extraction (open Q) | `DesktopRichTextViewer` + commons `RichTextParser` already render feed cards | **Reuse as-is** (~5-line wrap). Resolved. | -| Block = NIP-51 kind **30382** | 30382 = NIP-85 `ContactCardEvent` (WoT). Block = `PeopleListEvent` **30000**, d=`mute` | **Corrected throughout.** | +| Block = NIP-51 kind **30382** | 30382 = NIP-85 `UserAssertionEvent` (WoT). Block = `FollowSetEvent` **30000**, d=`mute` | **Corrected throughout.** | ## Base & Branch Strategy @@ -107,7 +107,7 @@ Phases 1–2 (banner, bio, CLINK, tabs) **don't depend on moderation-safety** an - **DM** → open Desktop Messages for this pubkey. - **Share** (copy `npub` / `nostr:` link). - **Add to list / follow-pack** (reuse Desktop Follow Packs). -- **Block** — `PeopleListEvent` **kind 30000, d=`mute`** (encrypted), distinct from the mute list; +- **Block** — `FollowSetEvent` **kind 30000, d=`mute`** (encrypted), distinct from the mute list; + Unblock. (Reuses existing quartz builder — see Technical Approach.) - **Edit Profile:** add the missing **CLINK offer** field (`noffer1…`). @@ -159,7 +159,7 @@ Write-path template must guard `pubKeyHex == account.signer.pubKey` (self) and ` | Edit dialog (add CLINK) | `desktopApp/.../desktop/ui/profile/EditProfileScreen.kt` + commons `profile/EditProfileFields.kt` | | Follow Packs (add-to-list target) | `desktopApp/.../desktop/followpacks/` | | Desktop Messages (DM target) | `desktopApp/.../desktop/ui/chats/` (`ChatroomListState.selectRoom(build1on1(pubkey))`) | -| **Block — reuse, don't rebuild** | quartz `nip51Lists/peopleList/PeopleListEvent.kt` (`KIND=30000`, `BLOCK_LIST_D_TAG="mute"`, `addUser(earlierVersion, isPrivate=true)`); Android `blockPeopleList/BlockPeopleListState.kt`; commons `PeopleListDecryptionCache.kt` | +| **Block — reuse, don't rebuild** | quartz `nip51Lists/followSet/FollowSetEvent.kt` (`KIND=30000`, `BLOCK_LIST_D_TAG="mute"`, `addUser(earlierVersion, isPrivate=true)`); Android `blockPeopleList/BlockPeopleListState.kt`; commons `FollowSetDecryptionCache.kt` | | Zap decryption | quartz `PrivateZapCache(signer)` / `PrivateZapRequestBuilder.decryptZapEvent`; Android `UserProfileZapsViewModel` | | Commons actions (pattern) | `commons/.../actions/FollowActions.kt`, `commons/.../model/nip56Reports/ReportAction.kt` | @@ -193,7 +193,7 @@ if (!account.isWriteable()) return // read-only/watch-only: no-op, U // 2. NIP-51 lists (Block): reuse existing decrypt→merge→encrypt; NEVER create() an existing list val current = blockListState.getBlockList() // must be LOADED from relays first (see guard) -val event = PeopleListEvent.addUser(earlierVersion = current, pubKeyHex, relayHint, isPrivate = true, signer) +val event = FollowSetEvent.addUser(earlierVersion = current, pubKeyHex, relayHint, isPrivate = true, signer) // fail-closed: addUser throws UnauthorizedDecryptionException rather than dropping private entries // 3. publish. Block = security-relevant → confirm, don't fire-and-forget @@ -247,7 +247,7 @@ account.justConsumeMyOwnEvent(event) // optimistic local apply, symmetric wit - **Add to list:** `commons/actions/FollowPackActions.kt` (build/publish kind 39089) + pack-picker modal; **zero-packs → offer "Create new pack"** (no dead-end). - **Block:** thin `commons/actions/BlockActions.kt` adapter over - `PeopleListEvent.addUser(earlierVersion, isPrivate=true)`; route write through `DesktopIAccount` + `FollowSetEvent.addUser(earlierVersion, isPrivate=true)`; route write through `DesktopIAccount` symmetric with `updateMuteList`; add to header overflow menu; enforce via unified hidden-set. ### Phase 4 — Tests & manual sheet @@ -334,9 +334,9 @@ Every tab defines **loading / empty / populated**; Followers + Zaps add a fourth - `amethyst/.../profile/mutual/dal/UserProfileMutualFeedFilter.kt` ### Verified shared / quartz -- `quartz/.../nip51Lists/peopleList/PeopleListEvent.kt` (KIND 30000, d=`mute`, `addUser`) +- `quartz/.../nip51Lists/followSet/FollowSetEvent.kt` (KIND 30000, d=`mute`, `addUser`) - `amethyst/.../model/nip51Lists/blockPeopleList/BlockPeopleListState.kt` -- `commons/.../model/nip51Lists/peopleList/PeopleListDecryptionCache.kt` +- `commons/.../model/nip51Lists/followSets/FollowSetDecryptionCache.kt` - quartz `PrivateZapCache` / `PrivateZapRequestBuilder.decryptZapEvent` - `commons/.../actions/FollowActions.kt`, `commons/.../model/nip56Reports/ReportAction.kt` - `commons/.../richtext/RichTextParser.kt`, `desktopApp/.../ui/note/DesktopRichTextViewer.kt` diff --git a/desktopApp/plans/2026-07-27-feat-desktop-profile-tabs-plan.md b/desktopApp/plans/2026-07-27-feat-desktop-profile-tabs-plan.md index 583efaa3fa..c7dfd9a6bc 100644 --- a/desktopApp/plans/2026-07-27-feat-desktop-profile-tabs-plan.md +++ b/desktopApp/plans/2026-07-27-feat-desktop-profile-tabs-plan.md @@ -56,7 +56,7 @@ override val privateZapsDecryptionCache: IPrivateZapsDecryptionCache = PrivateZa lazily decrypts per event via `signer.decryptZapEvent`. Gate on `isWriteable()`; decrypt **own-profile only** (Android `UserProfileZapsViewModel:71-85`: `if (user.pubkeyHex == account.pubKey)` decrypt else fall back to `zapRequest.pubKey`). Lazy per row — a NIP-46 bunker does one round-trip -per zap. Kinds: `LnZapEvent.KIND=9735`, `OnchainZapEvent.KIND=8333`. +per zap. Kinds: `ZapReceiptEvent.KIND=9735`, `OnchainZapEvent.KIND=8333`. ## Per-tab implementation diff --git a/desktopApp/plans/2026-08-06-shared-metadata-loading-upstream-reconciliation-plan.md b/desktopApp/plans/2026-08-06-shared-metadata-loading-upstream-reconciliation-plan.md index e30caa24c7..562dca4e0b 100644 --- a/desktopApp/plans/2026-08-06-shared-metadata-loading-upstream-reconciliation-plan.md +++ b/desktopApp/plans/2026-08-06-shared-metadata-loading-upstream-reconciliation-plan.md @@ -23,7 +23,7 @@ Upstream re-architected the same subsystem but **also completed the model-sharin - `watchers/UserWatcherSubAssembler.kt` — commons `BaseEoseManager`; reads `account.indexerRelayList.flow.value`; calls `filterUserMetadataForKey(...)`. - `watchers/FilterUserMetadataForKey.kt` — emits `ExplainedFilter(PROFILE_METADATA, …)`; reads `LocalCache.relayHints.hintsForKey(...)` **statically** (the one non-injected cache ref). - `watchers/UserReportsSubAssembler.kt` — commons `SingleSubEoseManager`; reads `account.declaredFollowsPerOutboxRelay.value`, `account.userProfile().pubkeyHex`. -- `watchers/UserCardsSubAssembler.kt` — commons `SingleSubEoseManager`; reads `account.homeRelays.flow.value`, `account.trustProviderList.liveUserRankProvider`, `…liveUserFollowerCount`, `account.userProfile().pubkeyHex`; emits `filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay(...)` (already in commons `assemblers/ContactCardFilters.kt`). +- `watchers/UserCardsSubAssembler.kt` — commons `SingleSubEoseManager`; reads `account.homeRelays.flow.value`, `account.trustProviderList.liveUserRankProvider`, `…liveUserFollowerCount`, `account.userProfile().pubkeyHex`; emits `filterUserAssertionsToTargetKeysFromTrustedAccountsInTheRelay(...)` (already in commons `assemblers/ContactCardFilters.kt`). ### Event side — `amethyst/.../reqCommand/event/` - `EventFinderFilterAssembler.kt` — `EventFinderQueryState(note, override val account: Account) : AccountScopedQuery`; groups `NoteEventLoaderSubAssembler`, `EventWatcherSubAssembler`, `AddressableAuthorRelayLoaderSubAssembler(cache, ::allKeys, userFinder)`. diff --git a/desktopApp/plans/archive/2026-05-09-embedded-local-relay-plan.md b/desktopApp/plans/archive/2026-05-09-embedded-local-relay-plan.md index 72cfbb3080..6d675db17a 100644 --- a/desktopApp/plans/archive/2026-05-09-embedded-local-relay-plan.md +++ b/desktopApp/plans/archive/2026-05-09-embedded-local-relay-plan.md @@ -290,7 +290,7 @@ SQLiteEventStore's `DeletionRequestModule` already handles this via BEFORE INSER - When kind 5 event inserted → trigger deletes referenced events - Trigger also prevents re-insertion of deleted events -**Desktop hook**: In `DesktopLocalCache.consume()`, when a `DeletionEvent` is consumed, the write-through to `LocalRelayStore.enqueue()` handles it — the store's trigger does the rest. +**Desktop hook**: In `DesktopLocalCache.consume()`, when a `DeletionRequestEvent` is consumed, the write-through to `LocalRelayStore.enqueue()` handles it — the store's trigger does the rest. #### Pruning Strategy Add to `LocalRelayStore`: diff --git a/desktopApp/plans/archive/2026-05-11-wallet-zapping-brainstorm.md b/desktopApp/plans/archive/2026-05-11-wallet-zapping-brainstorm.md index 102d27904e..a9435ffd03 100644 --- a/desktopApp/plans/archive/2026-05-11-wallet-zapping-brainstorm.md +++ b/desktopApp/plans/archive/2026-05-11-wallet-zapping-brainstorm.md @@ -44,7 +44,7 @@ - Biometric auth for sensitive operations ### Shared Infrastructure (Already in quartz/commons) -- **NIP-57:** `LnZapEvent`, `LnZapRequestEvent`, `LnZapPrivateEvent`, private zap encryption +- **NIP-57:** `ZapReceiptEvent`, `ZapRequestEvent`, `PrivateZapEvent`, private zap encryption - **NIP-47:** `Nip47Client`, request/response events, full RPC method set - **NIP-60:** `CashuWalletEvent`, `CashuTokenEvent`, `CashuSpendingHistoryEvent` (skeleton) - **NIP-61:** `NutzapEvent`, `NutzapInfoEvent` (skeleton) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt index 39db2557dd..40736a1fa6 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/cache/DesktopLocalCache.kt @@ -58,7 +58,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcResponseEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent @@ -331,7 +331,7 @@ class DesktopLocalCache : ICacheProvider { } is LongFormContentEvent -> { - consumeLongTextNote(event, relay) + consumeLongFormContent(event, relay) } is BookmarkListEvent -> { @@ -343,7 +343,7 @@ class DesktopLocalCache : ICacheProvider { } is StarterPackEvent -> { - consumeFollowList(event) + consumeStarterPack(event) } is CommentEvent -> { @@ -355,7 +355,7 @@ class DesktopLocalCache : ICacheProvider { } is DmRelayListEvent -> { - consumeChatMessageRelayList(event, relay) + consumeDmRelayList(event, relay) } is BlossomServersEvent -> { @@ -435,7 +435,7 @@ class DesktopLocalCache : ICacheProvider { * indexer fan-out. Mirrors [consumeAdvertisedRelayList]; emits nothing to * the event stream because the UI doesn't render kind 10050s directly. */ - private fun consumeChatMessageRelayList( + private fun consumeDmRelayList( event: DmRelayListEvent, relay: NormalizedRelayUrl?, ): Boolean { @@ -738,7 +738,7 @@ class DesktopLocalCache : ICacheProvider { * Consumes a kind 30023 long-form text note event. * Creates Note in cache like TextNoteEvent. */ - private fun consumeLongTextNote( + private fun consumeLongFormContent( event: LongFormContentEvent, relay: NormalizedRelayUrl?, ): Boolean { @@ -788,7 +788,7 @@ class DesktopLocalCache : ICacheProvider { private val _followPackVersion = MutableStateFlow(0L) val followPackVersion: StateFlow<Long> = _followPackVersion.asStateFlow() - private fun consumeFollowList(event: StarterPackEvent): Boolean { + private fun consumeStarterPack(event: StarterPackEvent): Boolean { val address = event.address() val addressableNote = getOrCreateAddressableNote(address) val author = getOrCreateUser(event.pubKey) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/BulkFollowAction.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/BulkFollowAction.kt index 440f4aee46..662984deaf 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/BulkFollowAction.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/BulkFollowAction.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache import com.vitorpamplona.amethyst.desktop.model.DesktopIAccount import com.vitorpamplona.amethyst.desktop.network.RelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent data class BulkFollowPreview( val totalMembers: Int, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/FollowPackEditor.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/FollowPackEditor.kt index d08cf586f1..50ab804017 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/FollowPackEditor.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/FollowPackEditor.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.desktop.followpacks import com.vitorpamplona.quartz.nip01Core.core.Address -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent /** * Lightweight helpers for follow-pack viewing actions. diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/FollowPacksState.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/FollowPacksState.kt index baac2023fc..cbb0465f48 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/FollowPacksState.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/FollowPacksState.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/DiscoverScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/DiscoverScreen.kt index ad675bc830..fb3f820b52 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/DiscoverScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/DiscoverScreen.kt @@ -71,7 +71,7 @@ import com.vitorpamplona.amethyst.desktop.model.DesktopIAccount import com.vitorpamplona.amethyst.desktop.network.RelayConnectionManager import com.vitorpamplona.amethyst.desktop.ui.ZapFeedback import com.vitorpamplona.amethyst.desktop.ui.live.LivesSection -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import kotlinx.coroutines.launch import java.awt.Toolkit import java.awt.datatransfer.StringSelection diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowAllConfirmDialog.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowAllConfirmDialog.kt index 16f8d6b122..be3285ac81 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowAllConfirmDialog.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowAllConfirmDialog.kt @@ -42,7 +42,7 @@ import com.vitorpamplona.amethyst.desktop.followpacks.BulkFollowAction import com.vitorpamplona.amethyst.desktop.followpacks.BulkFollowPreview import com.vitorpamplona.amethyst.desktop.model.DesktopIAccount import com.vitorpamplona.amethyst.desktop.network.RelayConnectionManager -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import kotlinx.coroutines.delay import kotlinx.coroutines.launch diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackBrowseAllScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackBrowseAllScreen.kt index 4dfd5696ad..33a0c974bc 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackBrowseAllScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackBrowseAllScreen.kt @@ -53,7 +53,7 @@ import com.vitorpamplona.amethyst.desktop.followpacks.FollowPackEditor import com.vitorpamplona.amethyst.desktop.followpacks.FollowPacksState import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent /** * Search-all view for follow packs. diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackComponents.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackComponents.kt index 5a0524f2ab..f684915b26 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackComponents.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackComponents.kt @@ -62,7 +62,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.ui.components.UserAvatar import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent /** * Stack of overlapping circular avatars for pack members. diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackDetailScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackDetailScreen.kt index b7f82a76b3..f925e3ab3e 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackDetailScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/FollowPackDetailScreen.kt @@ -68,7 +68,7 @@ import com.vitorpamplona.amethyst.desktop.followpacks.subscribeMetadataFor import com.vitorpamplona.amethyst.desktop.model.DesktopIAccount import com.vitorpamplona.amethyst.desktop.network.RelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import kotlinx.coroutines.launch /** diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/RenderFollowPackCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/RenderFollowPackCard.kt index 255e223ef7..1821eaf2d7 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/RenderFollowPackCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/followpacks/ui/RenderFollowPackCard.kt @@ -58,7 +58,7 @@ import com.vitorpamplona.amethyst.desktop.model.DesktopIAccount import com.vitorpamplona.amethyst.desktop.network.RelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress as NAddressEntity /** diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopHiddenUsersState.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopHiddenUsersState.kt index b298746177..ed87d85e64 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopHiddenUsersState.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopHiddenUsersState.kt @@ -23,11 +23,11 @@ package com.vitorpamplona.amethyst.desktop.model import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers import com.vitorpamplona.amethyst.commons.model.NoteState import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider +import com.vitorpamplona.amethyst.commons.model.nip51Lists.followSets.FollowSetDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip51Lists.muteList.MuteListDecryptionCache -import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleListDecryptionCache import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.utils.DualCase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -48,7 +48,7 @@ import kotlinx.coroutines.flow.stateIn * feeds to [com.vitorpamplona.amethyst.commons.model.Note.isHiddenFor]. * * Both lists carry a mix of public tags and an NIP-44-encrypted private section; - * the shared [MuteListDecryptionCache]/[PeopleListDecryptionCache] handle the + * the shared [MuteListDecryptionCache]/[FollowSetDecryptionCache] handle the * async decrypt (and are no-ops for read-only accounts, which simply see the * public portion). The result is exposed as a hot [StateFlow] so feeds can * re-filter live: when the list events change (or decryption resolves), a new @@ -65,7 +65,7 @@ class DesktopHiddenUsersState( private val showSensitiveContent: StateFlow<Boolean?> = MutableStateFlow(null), ) { private val muteCache = MuteListDecryptionCache(signer) - private val blockCache = PeopleListDecryptionCache(signer) + private val blockCache = FollowSetDecryptionCache(signer) // Strong refs so the GC keeps these addressable notes (and their decrypt caches) alive. private val muteListNote = cache.getOrCreateAddressableNote(MuteListEvent.createAddress(signer.pubKey)) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt index 7264e2d71b..a690bd1032 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt @@ -125,8 +125,8 @@ import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.identityClaims import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt index 6c6e49ad14..257baa799b 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt @@ -376,7 +376,7 @@ private fun RenderBechSegment( .NAddress .parse(segment.segmentText) } - if (naddr != null && naddr.kind == com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent.KIND) { + if (naddr != null && naddr.kind == com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent.KIND) { val followPacks = com.vitorpamplona.amethyst.desktop.ui.deck.LocalFollowPacksState.current val relayManager = com.vitorpamplona.amethyst.desktop.ui.deck.LocalRelayManager.current if (localCache != null && relayManager != null && followPacks != null) { diff --git a/docs/TODO.md b/docs/TODO.md index 979b4c9996..a9711dbd38 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -5,7 +5,7 @@ **Question:** What happens when a conversation with an npub has both NIP-04 (kind 4) and NIP-17 (kind 14/15 in GiftWrap) messages? **Current behavior to investigate:** -- NIP-04 messages use `PrivateDmEvent.chatroomKey(pubKey)` → creates a `ChatroomKey` based on recipient +- NIP-04 messages use `EncryptedDmEvent.chatroomKey(pubKey)` → creates a `ChatroomKey` based on recipient - NIP-17 messages use `ChatMessageEvent.chatroomKey(pubKey)` → also creates a `ChatroomKey` based on group members - Do these produce the **same** `ChatroomKey` for 1-on-1 chats? If yes, both message types merge into one conversation. If no, they appear as separate conversations. - Android Amethyst handles this — check how `Account.kt` merges them diff --git a/docs/brainstorms/2026-03-16-desktop-media-brainstorm.md b/docs/brainstorms/2026-03-16-desktop-media-brainstorm.md index 7793b10901..e3d503140a 100644 --- a/docs/brainstorms/2026-03-16-desktop-media-brainstorm.md +++ b/docs/brainstorms/2026-03-16-desktop-media-brainstorm.md @@ -136,7 +136,7 @@ ExoPlayer is Android-only (Media3). VLCJ wraps VLC's libvlc via JNA — supports | BlossomServersEvent | `quartz/nipB7Blossom/` | Ready (kind 10063) | | BlossomUri | `quartz/nipB7Blossom/` | Ready (blossom: URI parsing) | | BlossomUploadResult | `quartz/nipB7Blossom/` | Ready | -| FileHeaderEvent (NIP-94) | `quartz/nip94FileMetadata/` | Ready (kind 1063) | +| FileMetadataEvent (NIP-94) | `quartz/nip94FileMetadata/` | Ready (kind 1063) | | BlurhashTag | `quartz/nip94FileMetadata/tags/` | Ready | | DimensionTag | `quartz/nip94FileMetadata/tags/` | Ready | | IMetaTag/Builder (NIP-92) | `quartz/nip92IMeta/` | Ready | diff --git a/docs/brainstorms/2026-03-19-dm-encryption-badge-brainstorm.md b/docs/brainstorms/2026-03-19-dm-encryption-badge-brainstorm.md index e106e509db..221f8a1500 100644 --- a/docs/brainstorms/2026-03-19-dm-encryption-badge-brainstorm.md +++ b/docs/brainstorms/2026-03-19-dm-encryption-badge-brainstorm.md @@ -24,7 +24,7 @@ Users need to know which messages are truly private (NIP-17: relay can't see sen ## Implementation The badge goes in `MessageWithReactions` in ChatPane.kt, in the `detailRow` slot of `ChatMessageCompose`. Check `note.event` type: -- `is PrivateDmEvent` → NIP-04 → lock-open gray +- `is EncryptedDmEvent` → NIP-04 → lock-open gray - `is ChatMessageEvent` or `is ChatMessageEncryptedFileHeaderEvent` → NIP-17 → lock primary - else → no badge diff --git a/docs/changelog/v0.31.04.md b/docs/changelog/v0.31.04.md index ef95bfdc15..4f28425146 100644 --- a/docs/changelog/v0.31.04.md +++ b/docs/changelog/v0.31.04.md @@ -3,7 +3,7 @@ - Login with QR by [@greenart7c3](https://github.com/greenart7c3) - Adds full nip19 parsing on key decoding capabilities by [@vitorpamplona](https://github.com/vitorpamplona) - Speeds up RichText rendering parsers by [@vitorpamplona](https://github.com/vitorpamplona) -- added message to LnZapEvent and interface by [@believethehype](https://github.com/believethehype) +- added message to ZapReceiptEvent and interface by [@believethehype](https://github.com/believethehype) - Faster processing of Contact List events by [@vitorpamplona](https://github.com/vitorpamplona) - Faster user in follow list algo by [@vitorpamplona](https://github.com/vitorpamplona) diff --git a/docs/changelog/v0.76.01.md b/docs/changelog/v0.76.01.md index 592371ba62..8e6a2a72e8 100644 --- a/docs/changelog/v0.76.01.md +++ b/docs/changelog/v0.76.01.md @@ -14,7 +14,7 @@ Adds support for Zap Split setup and payment via NWC and manually via wallet cal - Updated translations for cs/de/sv - Forces valid hexes in p- and e- tags when receiving the event - Stops accepting space as a Valid hex char and requires an even number of chars (padding) -- Forces the ZapRequest to exist when processing the LnZapEvent +- Forces the ZapRequest to exist when processing the ZapReceiptEvent - Fixes some long NIP-19 cutting off in notes - Updates to Finnish, Swedish, Czech and German translations - Adds Bengali, Bangladesh translations diff --git a/docs/changelog/v1.03.00.md b/docs/changelog/v1.03.00.md index 5b4cfbe6f7..2a2942030b 100644 --- a/docs/changelog/v1.03.00.md +++ b/docs/changelog/v1.03.00.md @@ -12,7 +12,7 @@ Improvements: - Improves the list of visible authors on live stream bubbles - Adds the error status to the URL normalizer to avoid checking it again - Adds a follow list state from kind 3 to keep following users in memory. -- Moves from RelationshipStatus to ContactCard as per NIP update +- Moves from RelationshipStatus to UserAssertion as per NIP update Fixes: - Removes crashing relay URIs with null-encoded bytes (%00) diff --git a/docs/changelog/v1.12.00.md b/docs/changelog/v1.12.00.md index efc05f16c2..f350e4e6ed 100644 --- a/docs/changelog/v1.12.00.md +++ b/docs/changelog/v1.12.00.md @@ -267,7 +267,7 @@ Highlights: - **Amethyst → commons migration (KMP/iOS).** - Moves shared, CLI-safe code out of the Android app into `commons`. - Decryption caches (Mute/People/Community, Hashtag, Favorite-algo, Trust-provider). - - Models: InterestSet, LabeledBookmarkList, NwcWalletEntry, CashuToken, OwnedEmojiPack. + - Models: InterestSet, BookmarkSet, NwcWalletEntry, CashuToken, OwnedEmojiPack. - The link-preview fetcher, relay broadcast tracker, and CLI-safe util extensions. - `HtmlParser` made KMP (dropped its Java `Charset` dependency). - A consolidated package taxonomy with a new architecture doc. diff --git a/docs/changelog/v1.12.02.md b/docs/changelog/v1.12.02.md index 3a8cb91df0..0c6e2f4b50 100644 --- a/docs/changelog/v1.12.02.md +++ b/docs/changelog/v1.12.02.md @@ -20,7 +20,7 @@ Highlights: - Adds a pausable, resumable full-text-search reindex to the SQLite and filesystem event stores, and clamps non-positive reindex batch sizes. - Indexes direct message content (NIP-17 ChatMessageEvent). -- Indexes zap comments (LnZap request/receipt, on-chain zap, and nutzap). +- Indexes zap comments (ZapReceipt request/receipt, on-chain zap, and nutzap). - Makes polls, code snippets, audio, and git/torrent replies and interest sets searchable. - Indexes parsed JSON fields of profile, channel, and app-handler events. - Indexes natural-language fields of more event kinds, and makes the remaining text-bearing events searchable. diff --git a/docs/plans/2026-06-10-feat-desktop-dm-reliability-plan.md b/docs/plans/2026-06-10-feat-desktop-dm-reliability-plan.md index 3046cbb196..9bb432f044 100644 --- a/docs/plans/2026-06-10-feat-desktop-dm-reliability-plan.md +++ b/docs/plans/2026-06-10-feat-desktop-dm-reliability-plan.md @@ -381,7 +381,7 @@ The bulk of the change lives in `quartz/.../nip01Core/relay/client/` (publish pa **Scope:** 1. **Shared `rumorCreatedAt` across recipient wraps** (R9): - - In `NIP17Factory.createWraps` (currently `quartz/.../nip17Dm/NIP17Factory.kt:43-72`), compute `rumorCreatedAt = TimeUtils.now()` once before the per-recipient `mapNotNullAsync` loop. Pass into every `SealedRumorEvent.create(...)` so all seals encode the same rumor (same `rumor.id`). + - In `NIP17Factory.createWraps` (currently `quartz/.../nip17Dm/NIP17Factory.kt:43-72`), compute `rumorCreatedAt = TimeUtils.now()` once before the per-recipient `mapNotNullAsync` loop. Pass into every `SealEvent.create(...)` so all seals encode the same rumor (same `rumor.id`). - Same `rumorId` becomes the dedupe anchor + receipt target across all recipients of a group message. 2. **Self-copy gift wrap to own DM relays** (R10): @@ -476,7 +476,7 @@ ComposeUI(send button click) → RecipientRelayFetcher.fetch([indexer relays]) → NIP17Factory.createWraps(text, recipients, signer) ── shared rumor_created_at (Phase 5) → for each recipient (parallel): - → SealedRumorEvent.create(rumor, recipient, signer) + → SealEvent.create(rumor, recipient, signer) → signer.sign(seal) (bunker round-trip if Remote) ── batch via Phase 6 capability → signer.nip44Encrypt(rumor, recipient) (bunker round-trip)── batch via Phase 6 capability → GiftWrapEvent.create(seal, recipient, ephemeralKey) diff --git a/docs/plans/archive/2026-03-18-feat-desktop-cache-navigation-persistence-plan.md b/docs/plans/archive/2026-03-18-feat-desktop-cache-navigation-persistence-plan.md index 40c4f29743..abc58a5be8 100644 --- a/docs/plans/archive/2026-03-18-feat-desktop-cache-navigation-persistence-plan.md +++ b/docs/plans/archive/2026-03-18-feat-desktop-cache-navigation-persistence-plan.md @@ -124,8 +124,8 @@ Start with 4 new event kinds (kind 9734 required for zap processing). Add remain | 0 | `MetadataEvent` | Done | Already exists (`consumeMetadata`) | | 1 | `TextNoteEvent` | 1 | Core feed content | | 7 | `ReactionEvent` | 1 | Reaction counts on Note | -| 9734 | `LnZapRequestEvent` | 1 | Required before kind 9735 can process | -| 9735 | `LnZapEvent` | 1 | Zap counts on Note | +| 9734 | `ZapRequestEvent` | 1 | Required before kind 9735 can process | +| 9735 | `ZapReceiptEvent` | 1 | Zap counts on Note | Each consume method follows Android's pattern. Use `event.tagsWithoutCitations()` for reply parsing (handles both NIP-10 marked and legacy positional tags, excluding inline nostr: citations): @@ -410,7 +410,7 @@ rememberSubscription(configuredRelays, feedMode, followedUsers, relayManager = r | UserProfileScreen | Parameterized (pubkey) | — | Uses existing kinds | | SearchResultsList | Parameterized (query) | — | Uses DesktopSearchFeedFilter | | BookmarksScreen | Singleton | 30078 (BookmarkList) | Bookmark state from events | -| ReadsScreen | Singleton | 30023 (LongTextNote) | Articles feed | +| ReadsScreen | Singleton | 30023 (LongFormContent) | Articles feed | | NotificationsScreen | Singleton | — | Uses DesktopNotificationFeedFilter | **FeedNoteCard rewrite** — done alongside FeedScreen migration (first screen). All subsequent screen migrations benefit. diff --git a/docs/plans/archive/2026-03-22-feat-clean-cache-single-source-of-truth-plan.md b/docs/plans/archive/2026-03-22-feat-clean-cache-single-source-of-truth-plan.md index efd1a1f98a..a72899608b 100644 --- a/docs/plans/archive/2026-03-22-feat-clean-cache-single-source-of-truth-plan.md +++ b/docs/plans/archive/2026-03-22-feat-clean-cache-single-source-of-truth-plan.md @@ -166,7 +166,7 @@ Four-phase migration: FeedScreen first (proves pattern), then expand cache cover ##### 1a. Expand `DesktopLocalCache.consume()` coverage -Currently handles: MetadataEvent, TextNoteEvent, ReactionEvent, LnZapRequestEvent, LnZapEvent. +Currently handles: MetadataEvent, TextNoteEvent, ReactionEvent, ZapRequestEvent, ZapReceiptEvent. **Add support for:** @@ -174,7 +174,7 @@ Currently handles: MetadataEvent, TextNoteEvent, ReactionEvent, LnZapRequestEven |------------|------|--------|-------| | `RepostEvent` | 6 | `consumeRepost()` | `Note.addBoost()` on target | | `ContactListEvent` | 3 | `consumeContactList()` | Updates `_followedUsers` StateFlow | -| `LongTextNoteEvent` | 30023 | `consumeLongTextNote()` | Creates Note like TextNote | +| `LongFormContentEvent` | 30023 | `consumeLongFormContent()` | Creates Note like TextNote | | `BookmarkListEvent` | 30001 | `consumeBookmarkList()` | Stores on `addressableNotes` or dedicated field | **File:** `desktopApp/.../cache/DesktopLocalCache.kt` @@ -461,7 +461,7 @@ when (val state = feedState) { ##### Phase 1 Acceptance Criteria -- [ ] `DesktopLocalCache.consume()` handles RepostEvent, ContactListEvent, LongTextNoteEvent, BookmarkListEvent +- [ ] `DesktopLocalCache.consume()` handles RepostEvent, ContactListEvent, LongFormContentEvent, BookmarkListEvent - [ ] consume() uses kind-based registry dispatch (not growing `when` block) - [ ] `followedUsers` is `MutableStateFlow` (not `@Volatile`) - [ ] Coordinator scope uses `SupervisorJob` + `CoroutineExceptionHandler` @@ -513,7 +513,7 @@ Apply the proven pattern from Phase 1 to all other screens. Each screen gets at ##### 2d. ReadsScreen - `DesktopFeedViewModel(DesktopReadsFeedFilter(cache), cache)` -- **Card rendering:** ReadsScreen needs `LongFormNoteCard` that extracts title/summary/topics from `Note.event as? LongTextNoteEvent`. Separate composable, not FeedNoteCard. +- **Card rendering:** ReadsScreen needs `LongFormNoteCard` that extracts title/summary/topics from `Note.event as? LongFormContentEvent`. Separate composable, not FeedNoteCard. **File:** `desktopApp/.../ui/ReadsScreen.kt` @@ -523,7 +523,7 @@ Apply the proven pattern from Phase 1 to all other screens. Each screen gets at ```kotlin fun Note.toNotificationItem(): NotificationItem? = when (event) { is ReactionEvent -> NotificationItem.Reaction(this) - is LnZapEvent -> NotificationItem.Zap(this) + is ZapReceiptEvent -> NotificationItem.Zap(this) is TextNoteEvent -> if (isReply) NotificationItem.Reply(this) else NotificationItem.Mention(this) else -> null } diff --git a/docs/plans/archive/2026-03-24-feat-article-highlights-notes-plan.md b/docs/plans/archive/2026-03-24-feat-article-highlights-notes-plan.md index 6c663793cc..64c71fbebc 100644 --- a/docs/plans/archive/2026-03-24-feat-article-highlights-notes-plan.md +++ b/docs/plans/archive/2026-03-24-feat-article-highlights-notes-plan.md @@ -286,7 +286,7 @@ Track mouse via `Modifier.pointerInput` + `awaitPointerEventScope` (pattern from object HighlightPublishAction { suspend fun publish( highlightText: String, - articleEvent: LongTextNoteEvent, + articleEvent: LongFormContentEvent, note: String?, signer: NostrSigner, ): HighlightEvent { @@ -323,8 +323,8 @@ object HighlightPublishAction { ##### NIP-09 Deletion for Published Highlights ```kotlin -suspend fun deleteHighlight(eventId: String, signer: NostrSigner): DeletionEvent { - return DeletionEvent.create( +suspend fun deleteHighlight(eventId: String, signer: NostrSigner): DeletionRequestEvent { + return DeletionRequestEvent.create( deleteEvents = listOf(eventId), deleteKinds = listOf(9802), reason = "User deleted highlight", diff --git a/docs/plans/archive/2026-04-21-feat-relay-config-parity-plan.md b/docs/plans/archive/2026-04-21-feat-relay-config-parity-plan.md index 51320c1701..4e5093464d 100644 --- a/docs/plans/archive/2026-04-21-feat-relay-config-parity-plan.md +++ b/docs/plans/archive/2026-04-21-feat-relay-config-parity-plan.md @@ -115,12 +115,12 @@ fun DmRelayEditor( - Simple relay list (no read/write split — DM relays are all-or-nothing) - Warning banner if empty: "No DM relays configured — DMs will use connected relays as fallback" -- "Save" builds `ChatMessageRelayListEvent` → sign → publish +- "Save" builds `DmRelayListEvent` → sign → publish - Security: highlight that these relays see your DM metadata **Data flow:** 1. Read current: `accountRelays.dmRelayList` StateFlow -2. Save: build `ChatMessageRelayListEvent.create(relays, signer)` → publish +2. Save: build `DmRelayListEvent.create(relays, signer)` → publish **Integration:** - Replace "DM Relays — coming soon" placeholder in `RelayConfigTab` @@ -197,7 +197,7 @@ All editors follow the same pattern — reuse `RelayListEditor` for the add/remo | Kind | Event Class | Tag Format | |------|------------|------------| | 10002 | `AdvertisedRelayListEvent` | `["r", "wss://...", "read"\|"write"]` | -| 10050 | `ChatMessageRelayListEvent` | `["relay", "wss://..."]` | +| 10050 | `DmRelayListEvent` | `["relay", "wss://..."]` | | 10007 | `SearchRelayListEvent` | `["relay", "wss://..."]` | | 10006 | `BlockedRelayListEvent` | `["relay", "wss://..."]` | @@ -214,7 +214,7 @@ All editors follow the same pattern — reuse `RelayListEditor` for the add/remo ### Phase 3b: DM Relays - [ ] DM section shows current kind 10050 relays - [ ] Warning if empty -- [ ] Save signs + publishes ChatMessageRelayListEvent +- [ ] Save signs + publishes DmRelayListEvent - [ ] Shows publish confirmation ### Phase 3c: Search Relays diff --git a/docs/plans/archive/2026-04-21-feat-relay-subscription-wiring-plan.md b/docs/plans/archive/2026-04-21-feat-relay-subscription-wiring-plan.md index 3226771755..95697cce76 100644 --- a/docs/plans/archive/2026-04-21-feat-relay-subscription-wiring-plan.md +++ b/docs/plans/archive/2026-04-21-feat-relay-subscription-wiring-plan.md @@ -144,7 +144,7 @@ val blockedRelayList: StateFlow<Set<NormalizedRelayUrl>> = _blockedRelayList.asS fun consumeIfRelevant(event: Event): Boolean { return when (event.kind) { - ChatMessageRelayListEvent.KIND -> { consumeDmRelayList(event as ChatMessageRelayListEvent); true } + DmRelayListEvent.KIND -> { consumeDmRelayList(event as DmRelayListEvent); true } SearchRelayListEvent.KIND -> { consumeSearchRelayList(event); true } BlockedRelayListEvent.KIND -> { consumeBlockedRelayList(event); true } else -> false diff --git a/docs/plans/archive/2026-04-22-feat-relay-subscription-wiring-plan.md b/docs/plans/archive/2026-04-22-feat-relay-subscription-wiring-plan.md index 497ceecb91..b69d557e79 100644 --- a/docs/plans/archive/2026-04-22-feat-relay-subscription-wiring-plan.md +++ b/docs/plans/archive/2026-04-22-feat-relay-subscription-wiring-plan.md @@ -61,10 +61,10 @@ private var lastDmCreatedAt = 0L fun consumeIfRelevant(event: Event): Boolean { return when (event.kind) { - ChatMessageRelayListEvent.KIND -> { + DmRelayListEvent.KIND -> { if (event.createdAt > lastDmCreatedAt) { lastDmCreatedAt = event.createdAt - consumeDmRelayList(event as ChatMessageRelayListEvent) + consumeDmRelayList(event as DmRelayListEvent) } true } diff --git a/docs/plans/archive/2026-04-23-feat-desktop-multi-account-support-plan.md b/docs/plans/archive/2026-04-23-feat-desktop-multi-account-support-plan.md index 43ecaa7cbd..68844ec0ae 100644 --- a/docs/plans/archive/2026-04-23-feat-desktop-multi-account-support-plan.md +++ b/docs/plans/archive/2026-04-23-feat-desktop-multi-account-support-plan.md @@ -399,7 +399,7 @@ The KMP expert found that `AccountSessionManager` and `AccountCacheState` **cann - Use `#p` tag filters (not `authors`) — listening *for* the account, not *from* it - **Exclude kind 7 reactions** — too noisy for background counts. Use NIP-45 count query at foreground activation if needed - **Include kind 6 reposts** — same filter as kind 1 mentions, relatively rare, worth tracking -- NIP-65 `readRelaysNorm()` for general notifications; kind 10050 `ChatMessageRelayListEvent` for DM-specific inbox relays +- NIP-65 `readRelaysNorm()` for general notifications; kind 10050 `DmRelayListEvent` for DM-specific inbox relays - Max 5 relay connections per inactive account (3 inbox + 2 DM) - Always set `since` field — prevents relay flooding on reconnect - Separate `RelayAuthenticator` for background connections (don't reuse active account's) diff --git a/docs/plans/archive/2026-05-04-fix-bunker-timeouts-and-decryption-plan.md b/docs/plans/archive/2026-05-04-fix-bunker-timeouts-and-decryption-plan.md index 1c6acd5ace..fbc4bc0c94 100644 --- a/docs/plans/archive/2026-05-04-fix-bunker-timeouts-and-decryption-plan.md +++ b/docs/plans/archive/2026-05-04-fix-bunker-timeouts-and-decryption-plan.md @@ -221,7 +221,7 @@ No `since` → relay reconnection replays old events. This is GOOD for our retry Current code: ```kotlin decryptedContent = when (event) { - is PrivateDmEvent -> { + is EncryptedDmEvent -> { try { event.decryptContent(account.signer) } catch (_: Exception) { @@ -245,7 +245,7 @@ if (eventContent != null) { ```kotlin decryptedContent = when (event) { - is PrivateDmEvent -> { + is EncryptedDmEvent -> { try { event.decryptContent(account.signer) } catch (_: Exception) { diff --git a/docs/plans/archive/2026-05-22-feat-desktop-note-action-bar-ux-plan.md b/docs/plans/archive/2026-05-22-feat-desktop-note-action-bar-ux-plan.md index 1ccfdf5fac..a333621aa3 100644 --- a/docs/plans/archive/2026-05-22-feat-desktop-note-action-bar-ux-plan.md +++ b/docs/plans/archive/2026-05-22-feat-desktop-note-action-bar-ux-plan.md @@ -146,7 +146,7 @@ val zapEntries = note.zaps.mapNotNull { (request, receipt) -> val sender = request.author?.toBestDisplayName() ?: request.event?.pubKey?.take(8) ?: return@mapNotNull null - val amount = (receipt?.event as? LnZapEvent)?.amount?.toLong() + val amount = (receipt?.event as? ZapReceiptEvent)?.amount?.toLong() ?: return@mapNotNull null Triple(sender, amount, request.event?.content?.ifBlank { null }) }.sortedByDescending { it.second } diff --git a/docs/plans/archive/2026-06-02-fix-desktop-feed-review-findings-plan.md b/docs/plans/archive/2026-06-02-fix-desktop-feed-review-findings-plan.md index d61e49c14a..17697b2eaa 100644 --- a/docs/plans/archive/2026-06-02-fix-desktop-feed-review-findings-plan.md +++ b/docs/plans/archive/2026-06-02-fix-desktop-feed-review-findings-plan.md @@ -39,8 +39,8 @@ Eight parallel agents resolved all 5 open questions and surfaced 3 plan revision debounce. - **Q4 — NoteActions.kt:264 formatSats:** sats, safe to swap to `amount.toZapAmount()`. Inputs are hardcoded preset amounts (line 111 - `ZAP_AMOUNTS = listOf(21L, 100L, ...)`) and `LnZapEvent.amount` which is - already sats (`LnZapEvent.kt:69`). + `ZAP_AMOUNTS = listOf(21L, 100L, ...)`) and `ZapReceiptEvent.amount` which is + already sats (`ZapReceiptEvent.kt:69`). - **Q5 — WalletColumnScreen.kt:979 formatSats:** intentional. Wallet shows precise balance with locale-aware grouping (`1,000,000`). Leave + add `// intentional` comment to prevent future drift. @@ -378,7 +378,7 @@ zapCount = if (note.zapsAmount > BigDecimal.ZERO) showAmount(note.zapsAmount) el **Edit:** `NoteActions.kt:264` — swap to `amount.toZapAmount()`. Verified sats (not msats): inputs are `ZAP_AMOUNTS = listOf(21L, 100L, 500L, 1000L, 5000L, 10000L)` -at line 111 + `LnZapEvent.amount` which is sats per `LnZapEvent.kt:69`. +at line 111 + `ZapReceiptEvent.amount` which is sats per `ZapReceiptEvent.kt:69`. **Delete the private `formatSats` fun if no remaining references** (run `grep -n formatSats desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt` after swap). diff --git a/quartz/plans/2026-06-12-giftwrap-deletion-requests.md b/quartz/plans/2026-06-12-giftwrap-deletion-requests.md index d2791d65db..f392c4d991 100644 --- a/quartz/plans/2026-06-12-giftwrap-deletion-requests.md +++ b/quartz/plans/2026-06-12-giftwrap-deletion-requests.md @@ -46,7 +46,7 @@ special case describes — does nothing. `GiftWrapEvent.recipientPubKey()` at unseal time, so the validation below works after the wrap note is GC'd. -3. **Live cascade (amethyst `LocalCache.consume(DeletionEvent)`):** the +3. **Live cascade (amethyst `LocalCache.consume(DeletionRequestEvent)`):** the wrap note that knew its `innerEventId` is GC'd by the time a deletion arrives, so find the rumor by reverse lookup: scan notes for `note.rumorHost?.id == deletedId` (precedent: the addressable pass in diff --git a/quartz/plans/2026-07-11-concord-event-classes.md b/quartz/plans/2026-07-11-concord-event-classes.md index 82e57ac9a4..76b1a1ce32 100644 --- a/quartz/plans/2026-07-11-concord-event-classes.md +++ b/quartz/plans/2026-07-11-concord-event-classes.md @@ -10,7 +10,7 @@ Two problems: 1. **Duplicates standard Nostr kinds.** `ConcordKinds.MESSAGE=9`, `REACTION=7`, `DELETE=5`, `COMMENT=1111`, `EDIT=3302` shadow `ChatEvent.KIND`, - `ReactionEvent.KIND`, `DeletionEvent.KIND`, `CommentEvent.KIND`. Concord chat + `ReactionEvent.KIND`, `DeletionRequestEvent.KIND`, `CommentEvent.KIND`. Concord chat rumors *are* those standard events (they already parse back as `ChatEvent` etc. on read) — the build side should reuse the classes, not re-derive by number. @@ -37,7 +37,7 @@ binding. Introduce a binding-tag package and reuse the standard builders: | message (9) | `ChatEvent.build` | `channel` + `epoch` | | reply (9 + q) | `ChatEvent.build` + `q`/`p` | `channel` + `epoch` | | reaction (7) | `ReactionEvent.build` | `channel` + `epoch` | -| delete (5) | `DeletionEvent.build` | `channel` + `epoch` | +| delete (5) | `DeletionRequestEvent.build` | `channel` + `epoch` | `ChannelChat` keeps its public API (returns unsigned rumors via `RumorAssembler`) but builds tags from the standard event's DSL + the binding ext. Delete diff --git a/quartz/plans/archive/2026-06-09-clink.md b/quartz/plans/archive/2026-06-09-clink.md index 3529780fe0..fff712d54a 100644 --- a/quartz/plans/archive/2026-06-09-clink.md +++ b/quartz/plans/archive/2026-06-09-clink.md @@ -59,7 +59,7 @@ tags, `["e", reqId]` on responses, NIP-44 JSON content, GFY error codes | NIP-44 encrypt/decrypt | ✅ Reuse | `quartz/.../nip44Encryption/` + `signer.nip44Encrypt/Decrypt` | Use as-is | | Bech32 + TLV codec | ✅ Reuse | `nip19Bech32/bech32/`, `tlv/TlvBuilder.kt`, `tlv/Tlv.kt` | Use the codec; do NOT reuse `Nip19Parser` | | Entity TLV pattern (reference) | 📦 Mirror | `nip19Bech32/entities/NProfile.kt` | Copy shape into new Clink entities | -| Encrypted req/resp event pattern | 📦 Mirror | `nip47WalletConnect/events/LnZapPaymentRequestEvent.kt` (kind 23194) | Clone for kinds 21001/2/3, client + server | +| Encrypted req/resp event pattern | 📦 Mirror | `nip47WalletConnect/events/NwcRequestEvent.kt` (kind 23194) | Clone for kinds 21001/2/3, client + server | | Request/response over relay | ✅ Reuse | `Nip47Client.responseFilter()`, `relayClient/reqCommand/nwc/` | Same filter-by-`e`-tag pattern | | BOLT-11 parsing | ✅ Reuse | `quartz/.../lightning/LnInvoiceUtil.kt` | Use as-is | | LN payment execution | ✅ Reuse | `amethyst/.../service/ZapPaymentHandler.kt`, `NwcSignerState` | Add CLINK routes | @@ -85,7 +85,7 @@ with `events/`, `pointers/`, `rpc/` subfolders. 3. **Events** — `OfferEvent(21001)`, `DebitEvent(21002)`, `ManageEvent(21003)`, each `isContentEncoded() = true`, with `createRequest()` / `createResponse()` companions that NIP-44-encrypt JSON and set `p` / `clink_version` / `e` tags - (direct analogue of `LnZapPaymentRequestEvent`). Register in `EventFactory`. + (direct analogue of `NwcRequestEvent`). Register in `EventFactory`. 4. **DTOs + errors** — Jackson request/response classes per spec, shared `GfyError(code, message, range?, retryAfter?, delta?)`, Offers error model (`code` 1..5, `range`, `latest`). diff --git a/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt b/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcRequestNip44EventTest.kt similarity index 98% rename from quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt rename to quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcRequestNip44EventTest.kt index e3e0232e4f..8758bed7ad 100644 --- a/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt +++ b/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcRequestNip44EventTest.kt @@ -35,7 +35,7 @@ import kotlin.test.assertEquals import kotlin.test.assertIs @RunWith(AndroidJUnit4::class) -class LnZapPaymentRequestNip44EventTest { +class NwcRequestNip44EventTest { @Test fun testCreateRequestWithNip44() = runTest { diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/SignerResult.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/SignerResult.kt index 2191d6a903..63dae210a5 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/SignerResult.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/SignerResult.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip55AndroidSigner.api import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent sealed interface SignerResult<T : IResult> { sealed interface RequestAddressed<T : IResult> : SignerResult<T> { @@ -83,7 +83,7 @@ data class DecryptionResult( ) : IResult data class ZapEventDecryptionResult( - val privateEvent: LnZapPrivateEvent, + val privateEvent: PrivateZapEvent, ) : IResult data class DerivationResult( diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DecryptZapQuery.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DecryptZapQuery.kt index b41c591e9c..bb9cf0c4f7 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DecryptZapQuery.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DecryptZapQuery.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.getStringByName import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.query -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent class DecryptZapQuery( val loggedInUser: HexKey, @@ -45,7 +45,7 @@ class DecryptZapQuery( val decryptedEventAsJson = cursor.getStringByName("result") if (!decryptedEventAsJson.isNullOrBlank()) { if (decryptedEventAsJson.startsWith("{")) { - val event = Event.fromJsonOrNull(decryptedEventAsJson) as? LnZapPrivateEvent + val event = Event.fromJsonOrNull(decryptedEventAsJson) as? PrivateZapEvent if (event != null) { if (event.verify()) { SignerResult.RequestAddressed.Successful(ZapEventDecryptionResult(event)) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt index 38b7253126..f39e730a00 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt @@ -24,11 +24,11 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResult -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent class DecryptZapResponse { companion object { - fun assemble(event: LnZapPrivateEvent): IntentResult = + fun assemble(event: PrivateZapEvent): IntentResult = IntentResult( result = event.toJson(), ) @@ -40,7 +40,7 @@ class DecryptZapResponse { val eventJson = intent.result return if (!eventJson.isNullOrBlank()) { if (eventJson.startsWith("{")) { - val event = Event.fromJsonOrNull(eventJson) as? LnZapPrivateEvent + val event = Event.fromJsonOrNull(eventJson) as? PrivateZapEvent if (event != null) { SignerResult.RequestAddressed.Successful(ZapEventDecryptionResult(event)) } else { diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt index c5d1a6c388..218ca03dbc 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult import com.vitorpamplona.quartz.nip55AndroidSigner.client.handlers.BackgroundRequestHandler import com.vitorpamplona.quartz.nip55AndroidSigner.client.handlers.ForegroundRequestHandler -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent class NostrSignerExternal( @@ -163,7 +163,7 @@ class NostrSignerExternal( throw convertExceptions("Could not derive key", result) } - override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent { + override suspend fun decryptZapEvent(event: ZapRequestEvent): PrivateZapEvent { if (!event.isPrivateZap()) throw SignerExceptions.NothingToDecrypt() val result = backgroundQuery.decryptZapEvent(event) ?: foregroundQuery.decryptZapEvent(event) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt index fd07f11def..a4f4695cd4 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt @@ -161,21 +161,20 @@ import com.vitorpamplona.quartz.nip51Lists.appCurationSet.AppCurationSetEvent import com.vitorpamplona.quartz.nip51Lists.articleCurationSet.ArticleCurationSetEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.BookmarkSetEvent import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent import com.vitorpamplona.quartz.nip51Lists.gitAuthorList.GitAuthorListEvent import com.vitorpamplona.quartz.nip51Lists.gitRepositoryList.GitRepositoryListEvent import com.vitorpamplona.quartz.nip51Lists.goodWikiAuthorList.GoodWikiAuthorListEvent import com.vitorpamplona.quartz.nip51Lists.goodWikiRelayList.GoodWikiRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent import com.vitorpamplona.quartz.nip51Lists.kindMuteSet.KindMuteSetEvent -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent import com.vitorpamplona.quartz.nip51Lists.mediaFollowList.MediaFollowListEvent import com.vitorpamplona.quartz.nip51Lists.mediaStarterPack.MediaStarterPackEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.pictureCurationSet.PictureCurationSetEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent @@ -186,6 +185,7 @@ import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip51Lists.videoCurationSet.VideoCurationSetEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent @@ -204,7 +204,7 @@ import com.vitorpamplona.quartz.nip54Wiki.WikiMergeAcceptanceEvent import com.vitorpamplona.quartz.nip54Wiki.WikiMergeRequestEvent import com.vitorpamplona.quartz.nip54Wiki.WikiRedirectEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent @@ -452,7 +452,7 @@ object KindNames { ZapReceiptEvent.KIND to KindName("Zaps", "57"), NwcRequestEvent.KIND to KindName("NWC Request", "47"), NwcResponseEvent.KIND to KindName("NWC Response", "47"), - LnZapPrivateEvent.KIND to KindName("Private Zaps", "57"), + PrivateZapEvent.KIND to KindName("Private Zaps", "57"), ZapRequestEvent.KIND to KindName("Zap Req", "57"), LongFormContentEvent.KIND to KindName("Blogs", "23"), MeetingRoomEvent.KIND to KindName("Meeting Room", "53"), diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/MarmotInboundProcessor.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/MarmotInboundProcessor.kt index a1c6d5fb53..4110efe500 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/MarmotInboundProcessor.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/MarmotInboundProcessor.kt @@ -379,7 +379,7 @@ class MarmotInboundProcessor( /** * Process a WelcomeEvent after NIP-59 gift wrap unwrapping. * - * Called by the platform layer after unwrapping a GiftWrap → SealedRumor → WelcomeEvent. + * Called by the platform layer after unwrapping a GiftWrap → Seal → WelcomeEvent. * * Flow: * 1. Extract welcome bytes and KeyPackage event ID diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip02Welcome/WelcomeGiftWrap.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip02Welcome/WelcomeGiftWrap.kt index 2f4cc00f48..9558c6fc4d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip02Welcome/WelcomeGiftWrap.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip02Welcome/WelcomeGiftWrap.kt @@ -81,7 +81,7 @@ object WelcomeGiftWrap { // Step 2: Create a Rumor from the unsigned event and seal it (kind:13) val rumor = Rumor.create(welcomeRumor) - val sealedRumor = + val sealEvent = SealEvent.create( rumor = rumor, encryptTo = recipientPubKey, @@ -90,7 +90,7 @@ object WelcomeGiftWrap { // Step 3: Gift wrap (kind:1059) with an ephemeral key to the recipient return GiftWrapEvent.create( - event = sealedRumor, + event = sealEvent, recipientPubKey = recipientPubKey, ) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt index b87513ce80..bf50e9cd50 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.nip01Core.signers import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent abstract class NostrSigner( @@ -67,7 +67,7 @@ abstract class NostrSigner( fromPublicKey: HexKey, ): String - abstract suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent + abstract suspend fun decryptZapEvent(event: ZapRequestEvent): PrivateZapEvent abstract suspend fun deriveKey(nonce: HexKey): HexKey diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt index acb512e5c6..006fd3791c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import kotlinx.coroutines.CancellationException @@ -89,7 +89,7 @@ class NostrSignerInternal( // always ready override fun hasForegroundSupport() = true - override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent { + override suspend fun decryptZapEvent(event: ZapRequestEvent): PrivateZapEvent { if (!event.isPrivateZap()) throw SignerExceptions.NothingToDecrypt() return runWrapErrors { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt index 90566d4e7c..a821b95976 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04 import com.vitorpamplona.quartz.nip44Encryption.Nip44 -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent import com.vitorpamplona.quartz.nip57Zaps.PrivateZapRequestBuilder import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nipBCOnchainZaps.psbt.Psbt @@ -129,7 +129,7 @@ class NostrSignerSync( ) } - fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = PrivateZapRequestBuilder().decryptZapEvent(event, this) + fun decryptZapEvent(event: ZapRequestEvent): PrivateZapEvent = PrivateZapRequestBuilder().decryptZapEvent(event, this) /** * NIP-BC `sign_psbt`: sign the key-path P2TR inputs of [psbtHex] this key diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip13Pow/signer/PoWNostrSigner.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip13Pow/signer/PoWNostrSigner.kt index 3e9e00597d..770c08923d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip13Pow/signer/PoWNostrSigner.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip13Pow/signer/PoWNostrSigner.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip13Pow.miner.PoWMiner import com.vitorpamplona.quartz.nip13Pow.tags.PoWTag -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent /** @@ -98,7 +98,7 @@ class PoWNostrSigner( fromPublicKey: HexKey, ): String = signer.nip44Decrypt(ciphertext, fromPublicKey) - override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = signer.decryptZapEvent(event) + override suspend fun decryptZapEvent(event: ZapRequestEvent): PrivateZapEvent = signer.decryptZapEvent(event) override suspend fun deriveKey(nonce: HexKey): HexKey = signer.deriveKey(nonce) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListDiff.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/PublicChatListDiff.kt similarity index 84% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListDiff.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/PublicChatListDiff.kt index 69ab74f29c..3143cb4363 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListDiff.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/PublicChatListDiff.kt @@ -28,9 +28,15 @@ import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag /** Changes to the joined public chats: public channels as parsed tags, private ones as a whole. */ @Immutable -class ChannelListDiff( +class PublicChatListDiff( val channels: ListDiff<ChannelTag>, val privateItems: ContentChange, ) : EventDiff { override fun removesData() = privateItems.publicRemovalsAreLoss(channels.hasRemovals()) || privateItems.isRemoval() } + +@Deprecated( + "Renamed to PublicChatListDiff. Follows the ChannelListEvent to PublicChatListEvent rename (NIP-51 public chats list).", + ReplaceWith("PublicChatListDiff", "com.vitorpamplona.quartz.nip28PublicChat.list.PublicChatListDiff"), +) +typealias ChannelListDiff = PublicChatListDiff diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/PublicChatListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/PublicChatListEvent.kt index 08cdbbb345..78dd9fc4eb 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/PublicChatListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/PublicChatListEvent.kt @@ -50,11 +50,11 @@ class PublicChatListEvent( content: String, sig: HexKey, ) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig), - DiffableEvent<ChannelListDiff>, + DiffableEvent<PublicChatListDiff>, EventHintProvider { - override fun diffFrom(older: Event): ChannelListDiff? { + override fun diffFrom(older: Event): PublicChatListDiff? { if (older !is PublicChatListEvent || older.pubKey != pubKey || older.dTag() != dTag()) return null - return ChannelListDiff( + return PublicChatListDiff( ListDiff.of(older.tags.channels(), tags.channels(), { it.eventId }, { a, b -> a.relay == b.relay }), privateItemsChangeFrom(older), ) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt index f58bd0ad69..5a7820cc32 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt @@ -40,7 +40,7 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Encrypt import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestPing import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Log @@ -325,7 +325,7 @@ class NostrSignerRemote( throw convertExceptions("Could not get public key", result) } - override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent { + override suspend fun decryptZapEvent(event: ZapRequestEvent): PrivateZapEvent { TODO("Not yet implemented") } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractor.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractor.kt index e6176a77fb..139530d089 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractor.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractor.kt @@ -71,14 +71,14 @@ import com.vitorpamplona.quartz.nip51Lists.appCurationSet.AppCurationSetEvent import com.vitorpamplona.quartz.nip51Lists.articleCurationSet.ArticleCurationSetEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.BookmarkSetEvent +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent import com.vitorpamplona.quartz.nip51Lists.mediaStarterPack.MediaStarterPackEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.pictureCurationSet.PictureCurationSetEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip51Lists.videoCurationSet.VideoCurationSetEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent @@ -607,7 +607,7 @@ object SearchFieldExtractor { // kinds 1111/1311 -- body-only kinds whose indexableContent() // concatenates the `t` tags INTO the body. The funnel already // carries them in the hashtag role, so passing the body alone - // stops the same words being indexed twice (the ContactCard + // stops the same words being indexed twice (the UserAssertion // reasoning above, applied to the two chat/comment kinds). is CommentEvent -> { tiers(event, null, null, event.content) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/BookmarkSetEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkSet/BookmarkSetEvent.kt similarity index 97% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/BookmarkSetEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkSet/BookmarkSetEvent.kt index 01494372a3..318f74f5c5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/BookmarkSetEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkSet/BookmarkSetEvent.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList +package com.vitorpamplona.quartz.nip51Lists.bookmarkSet import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Address @@ -335,9 +335,3 @@ class BookmarkSetEvent( } } } - -@Deprecated( - "Renamed to BookmarkSetEvent. NIP-51 names kind 30003 bookmark sets.", - ReplaceWith("BookmarkSetEvent", "com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent"), -) -typealias LabeledBookmarkListEvent = BookmarkSetEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkSet/TagArrayBuilderExt.kt similarity index 97% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/TagArrayBuilderExt.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkSet/TagArrayBuilderExt.kt index 5754f4f9f2..568b662ff4 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkSet/TagArrayBuilderExt.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList +package com.vitorpamplona.quartz.nip51Lists.bookmarkSet import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/Deprecated.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/Deprecated.kt new file mode 100644 index 0000000000..f4ba91cd99 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/Deprecated.kt @@ -0,0 +1,90 @@ +/* + * 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. + */ +@file:Suppress("DEPRECATION") + +package com.vitorpamplona.quartz.nip51Lists.followList + +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.description as movedDescription +import com.vitorpamplona.quartz.nip51Lists.starterPack.followIdSet as movedFollowIdSet +import com.vitorpamplona.quartz.nip51Lists.starterPack.followIds as movedFollowIds +import com.vitorpamplona.quartz.nip51Lists.starterPack.follows as movedFollows +import com.vitorpamplona.quartz.nip51Lists.starterPack.image as movedImage +import com.vitorpamplona.quartz.nip51Lists.starterPack.people as movedPeople +import com.vitorpamplona.quartz.nip51Lists.starterPack.person as movedPerson +import com.vitorpamplona.quartz.nip51Lists.starterPack.personFirst as movedPersonFirst +import com.vitorpamplona.quartz.nip51Lists.starterPack.removePerson as movedRemovePerson +import com.vitorpamplona.quartz.nip51Lists.starterPack.title as movedTitle + +// This package was renamed to com.vitorpamplona.quartz.nip51Lists.starterPack (NIP-51 kind 39089 is a starter pack). +// Everything below forwards to it and will be removed in a future release. + +private const val MOVED = "Moved to com.vitorpamplona.quartz.nip51Lists.starterPack" + +@Deprecated( + "Renamed to StarterPackEvent and moved to com.vitorpamplona.quartz.nip51Lists.starterPack. NIP-51 kind 39089 is a starter pack.", + ReplaceWith("StarterPackEvent", "com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent"), +) +typealias FollowListEvent = StarterPackEvent + +@Deprecated(MOVED, ReplaceWith("title(title)", "com.vitorpamplona.quartz.nip51Lists.starterPack.title")) +fun TagArrayBuilder<StarterPackEvent>.title(title: String) = movedTitle(title) + +@Deprecated(MOVED, ReplaceWith("description(desc)", "com.vitorpamplona.quartz.nip51Lists.starterPack.description")) +fun TagArrayBuilder<StarterPackEvent>.description(desc: String) = movedDescription(desc) + +@Deprecated(MOVED, ReplaceWith("image(imageUrl)", "com.vitorpamplona.quartz.nip51Lists.starterPack.image")) +fun TagArrayBuilder<StarterPackEvent>.image(imageUrl: String) = movedImage(imageUrl) + +@Deprecated(MOVED, ReplaceWith("people(peoples)", "com.vitorpamplona.quartz.nip51Lists.starterPack.people")) +fun TagArrayBuilder<StarterPackEvent>.people(peoples: List<UserTag>) = movedPeople(peoples) + +@Deprecated(MOVED, ReplaceWith("person(person)", "com.vitorpamplona.quartz.nip51Lists.starterPack.person")) +fun TagArrayBuilder<StarterPackEvent>.person(person: UserTag) = movedPerson(person) + +@Deprecated(MOVED, ReplaceWith("person(pubkey, relayHint)", "com.vitorpamplona.quartz.nip51Lists.starterPack.person")) +fun TagArrayBuilder<StarterPackEvent>.person( + pubkey: HexKey, + relayHint: NormalizedRelayUrl?, +) = movedPerson(pubkey, relayHint) + +@Deprecated(MOVED, ReplaceWith("personFirst(pubkey, relayHint)", "com.vitorpamplona.quartz.nip51Lists.starterPack.personFirst")) +fun TagArrayBuilder<StarterPackEvent>.personFirst( + pubkey: HexKey, + relayHint: NormalizedRelayUrl?, +) = movedPersonFirst(pubkey, relayHint) + +@Deprecated(MOVED, ReplaceWith("removePerson(pubkey)", "com.vitorpamplona.quartz.nip51Lists.starterPack.removePerson")) +fun TagArrayBuilder<StarterPackEvent>.removePerson(pubkey: HexKey) = movedRemovePerson(pubkey) + +@Deprecated(MOVED, ReplaceWith("follows()", "com.vitorpamplona.quartz.nip51Lists.starterPack.follows")) +fun TagArray.follows() = movedFollows() + +@Deprecated(MOVED, ReplaceWith("followIds()", "com.vitorpamplona.quartz.nip51Lists.starterPack.followIds")) +fun TagArray.followIds() = movedFollowIds() + +@Deprecated(MOVED, ReplaceWith("followIdSet()", "com.vitorpamplona.quartz.nip51Lists.starterPack.followIdSet")) +fun TagArray.followIdSet() = movedFollowIdSet() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/FollowSetEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followSet/FollowSetEvent.kt similarity index 98% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/FollowSetEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followSet/FollowSetEvent.kt index d10d7de69b..fe0353dd48 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/FollowSetEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followSet/FollowSetEvent.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip51Lists.peopleList +package com.vitorpamplona.quartz.nip51Lists.followSet import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Address @@ -415,9 +415,3 @@ class FollowSetEvent( ) } } - -@Deprecated( - "Renamed to FollowSetEvent. NIP-51 names kind 30000 follow sets.", - ReplaceWith("FollowSetEvent", "com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent"), -) -typealias PeopleListEvent = FollowSetEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followSet/TagArrayBuilderExt.kt similarity index 97% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/TagArrayBuilderExt.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followSet/TagArrayBuilderExt.kt index 9a07db2fec..18e1ef4cba 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followSet/TagArrayBuilderExt.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip51Lists.peopleList +package com.vitorpamplona.quartz.nip51Lists.followSet import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followSet/TagArrayExt.kt similarity index 96% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/TagArrayExt.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followSet/TagArrayExt.kt index bad2b2563c..d0cd484542 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/TagArrayExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followSet/TagArrayExt.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip51Lists.peopleList +package com.vitorpamplona.quartz.nip51Lists.followSet import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/Deprecated.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/Deprecated.kt new file mode 100644 index 0000000000..417d0575b9 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/Deprecated.kt @@ -0,0 +1,61 @@ +/* + * 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. + */ +@file:Suppress("DEPRECATION") + +package com.vitorpamplona.quartz.nip51Lists.hashtagList + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListDiff +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent +import com.vitorpamplona.quartz.nip51Lists.interestList.followHashTag as movedFollowHashTag +import com.vitorpamplona.quartz.nip51Lists.interestList.hashtagList as movedHashtagList +import com.vitorpamplona.quartz.nip51Lists.interestList.hashtagSet as movedHashtagSet +import com.vitorpamplona.quartz.nip51Lists.interestList.hashtags as movedHashtags + +// This package was renamed to com.vitorpamplona.quartz.nip51Lists.interestList (NIP-51 kind 10015 is the interests list). +// Everything below forwards to it and will be removed in a future release. + +private const val MOVED = "Moved to com.vitorpamplona.quartz.nip51Lists.interestList" + +@Deprecated( + "Renamed to InterestListEvent and moved to com.vitorpamplona.quartz.nip51Lists.interestList. NIP-51 kind 10015 is the interests list.", + ReplaceWith("InterestListEvent", "com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent"), +) +typealias HashtagListEvent = InterestListEvent + +@Deprecated( + "Renamed to InterestListDiff and moved to com.vitorpamplona.quartz.nip51Lists.interestList. NIP-51 kind 10015 is the interests list.", + ReplaceWith("InterestListDiff", "com.vitorpamplona.quartz.nip51Lists.interestList.InterestListDiff"), +) +typealias HashtagListDiff = InterestListDiff + +@Deprecated(MOVED, ReplaceWith("followHashTag(hashtag)", "com.vitorpamplona.quartz.nip51Lists.interestList.followHashTag")) +fun TagArrayBuilder<InterestListEvent>.followHashTag(hashtag: String) = movedFollowHashTag(hashtag) + +@Deprecated(MOVED, ReplaceWith("hashtags(hashtags)", "com.vitorpamplona.quartz.nip51Lists.interestList.hashtags")) +fun TagArrayBuilder<InterestListEvent>.hashtags(hashtags: List<String>) = movedHashtags(hashtags) + +@Deprecated(MOVED, ReplaceWith("hashtagList()", "com.vitorpamplona.quartz.nip51Lists.interestList.hashtagList")) +fun TagArray.hashtagList() = movedHashtagList() + +@Deprecated(MOVED, ReplaceWith("hashtagSet()", "com.vitorpamplona.quartz.nip51Lists.interestList.hashtagSet")) +fun TagArray.hashtagSet() = movedHashtagSet() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/HashtagListDiff.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/interestList/InterestListDiff.kt similarity index 95% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/HashtagListDiff.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/interestList/InterestListDiff.kt index 21faacdee8..34284ce4db 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/HashtagListDiff.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/interestList/InterestListDiff.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip51Lists.hashtagList +package com.vitorpamplona.quartz.nip51Lists.interestList import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.diff.ContentChange @@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.diff.ListDiff /** Changes to the followed hashtags: public ones, and the private ones as a whole. */ @Immutable -class HashtagListDiff( +class InterestListDiff( val hashtags: ListDiff<String>, val privateItems: ContentChange, ) : EventDiff { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/InterestListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/interestList/InterestListEvent.kt similarity index 95% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/InterestListEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/interestList/InterestListEvent.kt index 950fb6c7a9..eaac7eaab7 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/InterestListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/interestList/InterestListEvent.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip51Lists.hashtagList +package com.vitorpamplona.quartz.nip51Lists.interestList import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Address @@ -49,10 +49,10 @@ class InterestListEvent( content: String, sig: HexKey, ) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig), - DiffableEvent<HashtagListDiff> { - override fun diffFrom(older: Event): HashtagListDiff? { + DiffableEvent<InterestListDiff> { + override fun diffFrom(older: Event): InterestListDiff? { if (older !is InterestListEvent || older.pubKey != pubKey || older.dTag() != dTag()) return null - return HashtagListDiff( + return InterestListDiff( ListDiff.of(older.publicHashtags(), publicHashtags(), { it.lowercase() }), privateItemsChangeFrom(older), ) @@ -216,9 +216,3 @@ class InterestListEvent( } } } - -@Deprecated( - "Renamed to InterestListEvent. NIP-51 names kind 10015 the interests list.", - ReplaceWith("InterestListEvent", "com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent"), -) -typealias HashtagListEvent = InterestListEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/interestList/TagArrayBuilderExt.kt similarity index 96% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/TagArrayBuilderExt.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/interestList/TagArrayBuilderExt.kt index 70977ca9b7..4de4cce0d9 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/interestList/TagArrayBuilderExt.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip51Lists.hashtagList +package com.vitorpamplona.quartz.nip51Lists.interestList import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/interestList/TagArrayExt.kt similarity index 96% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/TagArrayExt.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/interestList/TagArrayExt.kt index ec7d75dd35..9819d231a8 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/hashtagList/TagArrayExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/interestList/TagArrayExt.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip51Lists.hashtagList +package com.vitorpamplona.quartz.nip51Lists.interestList import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/Deprecated.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/Deprecated.kt new file mode 100644 index 0000000000..e420c86082 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/labeledBookmarkList/Deprecated.kt @@ -0,0 +1,58 @@ +/* + * 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. + */ +@file:Suppress("DEPRECATION") + +package com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag +import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.BookmarkSetEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.bookmarks as movedBookmarks +import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.description as movedDescription +import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.image as movedImage +import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.name as movedName +import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.title as movedTitle + +// This package was renamed to com.vitorpamplona.quartz.nip51Lists.bookmarkSet (NIP-51 kind 30003 is a bookmark set). +// Everything below forwards to it and will be removed in a future release. + +private const val MOVED = "Moved to com.vitorpamplona.quartz.nip51Lists.bookmarkSet" + +@Deprecated( + "Renamed to BookmarkSetEvent and moved to com.vitorpamplona.quartz.nip51Lists.bookmarkSet. NIP-51 kind 30003 is a bookmark set.", + ReplaceWith("BookmarkSetEvent", "com.vitorpamplona.quartz.nip51Lists.bookmarkSet.BookmarkSetEvent"), +) +typealias LabeledBookmarkListEvent = BookmarkSetEvent + +@Deprecated(MOVED, ReplaceWith("name(name)", "com.vitorpamplona.quartz.nip51Lists.bookmarkSet.name")) +fun TagArrayBuilder<BookmarkSetEvent>.name(name: String) = movedName(name) + +@Deprecated(MOVED, ReplaceWith("title(title)", "com.vitorpamplona.quartz.nip51Lists.bookmarkSet.title")) +fun TagArrayBuilder<BookmarkSetEvent>.title(title: String) = movedTitle(title) + +@Deprecated(MOVED, ReplaceWith("bookmarks(bookmarks)", "com.vitorpamplona.quartz.nip51Lists.bookmarkSet.bookmarks")) +fun TagArrayBuilder<BookmarkSetEvent>.bookmarks(bookmarks: List<BookmarkIdTag>) = movedBookmarks(bookmarks) + +@Deprecated(MOVED, ReplaceWith("description(listDescription)", "com.vitorpamplona.quartz.nip51Lists.bookmarkSet.description")) +fun TagArrayBuilder<BookmarkSetEvent>.description(listDescription: String) = movedDescription(listDescription) + +@Deprecated(MOVED, ReplaceWith("image(url)", "com.vitorpamplona.quartz.nip51Lists.bookmarkSet.image")) +fun TagArrayBuilder<BookmarkSetEvent>.image(url: String) = movedImage(url) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/mediaStarterPack/MediaStarterPackEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/mediaStarterPack/MediaStarterPackEvent.kt index 9cec42f0c3..5d862bfc41 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/mediaStarterPack/MediaStarterPackEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/mediaStarterPack/MediaStarterPackEvent.kt @@ -31,11 +31,11 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag import com.vitorpamplona.quartz.nip50Search.IndexableFieldVisitor import com.vitorpamplona.quartz.nip50Search.SearchableEvent -import com.vitorpamplona.quartz.nip51Lists.followList.followIdSet -import com.vitorpamplona.quartz.nip51Lists.followList.followIds -import com.vitorpamplona.quartz.nip51Lists.followList.follows import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag import com.vitorpamplona.quartz.nip51Lists.remove +import com.vitorpamplona.quartz.nip51Lists.starterPack.followIdSet +import com.vitorpamplona.quartz.nip51Lists.starterPack.followIds +import com.vitorpamplona.quartz.nip51Lists.starterPack.follows import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/Deprecated.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/Deprecated.kt new file mode 100644 index 0000000000..0e973677cc --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/peopleList/Deprecated.kt @@ -0,0 +1,83 @@ +/* + * 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. + */ +@file:Suppress("DEPRECATION") + +package com.vitorpamplona.quartz.nip51Lists.peopleList + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag +import com.vitorpamplona.quartz.nip51Lists.followSet.description as movedDescription +import com.vitorpamplona.quartz.nip51Lists.followSet.image as movedImage +import com.vitorpamplona.quartz.nip51Lists.followSet.name as movedName +import com.vitorpamplona.quartz.nip51Lists.followSet.peoples as movedPeoples +import com.vitorpamplona.quartz.nip51Lists.followSet.title as movedTitle +import com.vitorpamplona.quartz.nip51Lists.followSet.userIdSet as movedUserIdSet +import com.vitorpamplona.quartz.nip51Lists.followSet.userIds as movedUserIds +import com.vitorpamplona.quartz.nip51Lists.followSet.users as movedUsers +import com.vitorpamplona.quartz.nip51Lists.followSet.usersAndWords as movedUsersAndWords +import com.vitorpamplona.quartz.nip51Lists.followSet.wordSet as movedWordSet +import com.vitorpamplona.quartz.nip51Lists.followSet.words as movedWords + +// This package was renamed to com.vitorpamplona.quartz.nip51Lists.followSet (NIP-51 kind 30000 is a follow set). +// Everything below forwards to it and will be removed in a future release. + +private const val MOVED = "Moved to com.vitorpamplona.quartz.nip51Lists.followSet" + +@Deprecated( + "Renamed to FollowSetEvent and moved to com.vitorpamplona.quartz.nip51Lists.followSet. NIP-51 kind 30000 is a follow set.", + ReplaceWith("FollowSetEvent", "com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent"), +) +typealias PeopleListEvent = FollowSetEvent + +@Deprecated(MOVED, ReplaceWith("name(name)", "com.vitorpamplona.quartz.nip51Lists.followSet.name")) +fun TagArrayBuilder<FollowSetEvent>.name(name: String) = movedName(name) + +@Deprecated(MOVED, ReplaceWith("title(name)", "com.vitorpamplona.quartz.nip51Lists.followSet.title")) +fun TagArrayBuilder<FollowSetEvent>.title(name: String) = movedTitle(name) + +@Deprecated(MOVED, ReplaceWith("description(desc)", "com.vitorpamplona.quartz.nip51Lists.followSet.description")) +fun TagArrayBuilder<FollowSetEvent>.description(desc: String) = movedDescription(desc) + +@Deprecated(MOVED, ReplaceWith("image(url)", "com.vitorpamplona.quartz.nip51Lists.followSet.image")) +fun TagArrayBuilder<FollowSetEvent>.image(url: String) = movedImage(url) + +@Deprecated(MOVED, ReplaceWith("peoples(peoples)", "com.vitorpamplona.quartz.nip51Lists.followSet.peoples")) +fun TagArrayBuilder<FollowSetEvent>.peoples(peoples: List<UserTag>) = movedPeoples(peoples) + +@Deprecated(MOVED, ReplaceWith("usersAndWords()", "com.vitorpamplona.quartz.nip51Lists.followSet.usersAndWords")) +fun TagArray.usersAndWords() = movedUsersAndWords() + +@Deprecated(MOVED, ReplaceWith("users()", "com.vitorpamplona.quartz.nip51Lists.followSet.users")) +fun TagArray.users() = movedUsers() + +@Deprecated(MOVED, ReplaceWith("userIds()", "com.vitorpamplona.quartz.nip51Lists.followSet.userIds")) +fun TagArray.userIds() = movedUserIds() + +@Deprecated(MOVED, ReplaceWith("userIdSet()", "com.vitorpamplona.quartz.nip51Lists.followSet.userIdSet")) +fun TagArray.userIdSet() = movedUserIdSet() + +@Deprecated(MOVED, ReplaceWith("words()", "com.vitorpamplona.quartz.nip51Lists.followSet.words")) +fun TagArray.words() = movedWords() + +@Deprecated(MOVED, ReplaceWith("wordSet()", "com.vitorpamplona.quartz.nip51Lists.followSet.wordSet")) +fun TagArray.wordSet() = movedWordSet() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt index b3c2dee634..0e8a9db9ab 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAss import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.appId import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.assets import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.channel +import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.isNip82SoftwareRelease import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.version import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent @@ -70,15 +71,20 @@ class ReleaseArtifactSetEvent( EventHintProvider, AddressHintProvider, SearchableEvent { - override fun indexableContent() = listOfNotNull(title(), description()).joinToString("\n") + override fun indexableContent() = listOfNotNull(title(), description(), searchableReleaseNotes()).joinToString("\n") // The read path: the same fields indexableContent() joins, handed over without // building the joined string a scan would throw away. override fun forEachIndexableField(visitor: IndexableFieldVisitor) { if (!visitor.visit(title())) return - visitor.visit(description()) + if (!visitor.visit(description())) return + visitor.visit(searchableReleaseNotes()) } + // Only NIP-82 releases carry release notes in `content`. A NIP-51 set may keep + // encrypted private items there, which must never reach the index. + private fun searchableReleaseNotes() = if (isNip82SoftwareRelease()) content.ifBlank { null } else null + override fun eventHints() = tags.mapNotNull(EventBookmark::parseAsHint) override fun linkedEventIds() = tags.mapNotNull(EventBookmark::parseId) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/StarterPackEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/starterPack/StarterPackEvent.kt similarity index 95% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/StarterPackEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/starterPack/StarterPackEvent.kt index c38841df66..81fc1f760f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/StarterPackEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/starterPack/StarterPackEvent.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip51Lists.followList +package com.vitorpamplona.quartz.nip51Lists.starterPack import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Address @@ -184,9 +184,3 @@ class StarterPackEvent( } } } - -@Deprecated( - "Renamed to StarterPackEvent. Kind 39089 is a NIP-51 starter pack. The NIP-02 follow list is kind 3 (ContactListEvent).", - ReplaceWith("StarterPackEvent", "com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent"), -) -typealias FollowListEvent = StarterPackEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/starterPack/TagArrayBuilderExt.kt similarity index 97% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/TagArrayBuilderExt.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/starterPack/TagArrayBuilderExt.kt index 6b5cf9bc13..4fcace3cf1 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/starterPack/TagArrayBuilderExt.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip51Lists.followList +package com.vitorpamplona.quartz.nip51Lists.starterPack import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/starterPack/TagArrayExt.kt similarity index 96% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/TagArrayExt.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/starterPack/TagArrayExt.kt index e8f8a00bea..69303a2097 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/followList/TagArrayExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/starterPack/TagArrayExt.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip51Lists.followList +package com.vitorpamplona.quartz.nip51Lists.starterPack import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/IPrivateZapsDecryptionCache.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/IPrivateZapsDecryptionCache.kt index bae51c5c4f..3c36ab0fa2 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/IPrivateZapsDecryptionCache.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/IPrivateZapsDecryptionCache.kt @@ -25,7 +25,7 @@ package com.vitorpamplona.quartz.nip57Zaps * Used by Note.kt for checking private zap status. */ interface IPrivateZapsDecryptionCache { - fun cachedPrivateZap(event: ZapRequestEvent): LnZapPrivateEvent? + fun cachedPrivateZap(event: ZapRequestEvent): PrivateZapEvent? - suspend fun decryptPrivateZap(event: ZapRequestEvent): LnZapPrivateEvent? + suspend fun decryptPrivateZap(event: ZapRequestEvent): PrivateZapEvent? } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapCache.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapCache.kt index 57eb0a796c..29319eb223 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapCache.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapCache.kt @@ -43,16 +43,16 @@ class PrivateZapCache( decryptionCache.remove(event) } - override fun cachedPrivateZap(event: ZapRequestEvent): LnZapPrivateEvent? = decryptionCache[event]?.cached() + override fun cachedPrivateZap(event: ZapRequestEvent): PrivateZapEvent? = decryptionCache[event]?.cached() override suspend fun decryptPrivateZap(event: ZapRequestEvent) = decryptionCache[event]?.decrypt(event) } class PrivateZapDecryptCache( signer: NostrSigner, -) : DecryptCache<ZapRequestEvent, LnZapPrivateEvent>(signer) { +) : DecryptCache<ZapRequestEvent, PrivateZapEvent>(signer) { override suspend fun decryptAndParse( event: ZapRequestEvent, signer: NostrSigner, - ): LnZapPrivateEvent = signer.decryptZapEvent(event) + ): PrivateZapEvent = signer.decryptZapEvent(event) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapEvent.kt similarity index 87% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapEvent.kt index 09c88dd2a3..163f2415a1 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapEvent.kt @@ -34,7 +34,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class LnZapPrivateEvent( +class PrivateZapEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -66,13 +66,19 @@ class LnZapPrivateEvent( tags: Array<Array<String>> = emptyArray(), content: String = "", createdAt: Long = TimeUtils.now(), - ): LnZapPrivateEvent = signer.sign(createdAt, KIND, tags, content) + ): PrivateZapEvent = signer.sign(createdAt, KIND, tags, content) fun create( signer: NostrSignerSync, tags: Array<Array<String>> = emptyArray(), content: String = "", createdAt: Long = TimeUtils.now(), - ): LnZapPrivateEvent = signer.sign(createdAt, KIND, tags, content) + ): PrivateZapEvent = signer.sign(createdAt, KIND, tags, content) } } + +@Deprecated( + "Renamed to PrivateZapEvent. Follows the NIP-57 zap naming (ZapRequestEvent, ZapReceiptEvent).", + ReplaceWith("PrivateZapEvent", "com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent"), +) +typealias LnZapPrivateEvent = PrivateZapEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt index 595dc24600..a26cc784bd 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt @@ -56,7 +56,7 @@ class PrivateZapRequestBuilder { val fullTagsNoAnon = tags.filter { t -> t.getOrNull(0) != "anon" }.toTypedArray() - val privateEvent = LnZapPrivateEvent.create(signer, fullTagsNoAnon, content) + val privateEvent = PrivateZapEvent.create(signer, fullTagsNoAnon, content) val noteJson = privateEvent.toJson() val encryptedContent = @@ -75,7 +75,7 @@ class PrivateZapRequestBuilder { fun decryptZapEvent( event: ZapRequestEvent, signer: NostrSignerSync, - ): LnZapPrivateEvent { + ): PrivateZapEvent { if (signer.keyPair.privKey == null) throw SignerExceptions.ReadOnlyException() val recipientPK = event.zappedAuthor().firstOrNull() @@ -129,12 +129,12 @@ class PrivateZapRequestBuilder { encNote: String, privateKey: ByteArray, pubKey: HexKey, - ): LnZapPrivateEvent = + ): PrivateZapEvent = try { val note = PrivateZapEncryption.decryptPrivateZapMessage(encNote, privateKey, pubKey.hexToByteArray()) val decryptedEvent = fromJson(note) if (decryptedEvent.kind == 9733) { - decryptedEvent as LnZapPrivateEvent + decryptedEvent as PrivateZapEvent } else { throw IllegalStateException("The decrypted event is not a private zap.") } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/LnurlEndpointResolver.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/LnurlEndpointResolver.kt index 5b1841ada5..cb9cab5e9c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/LnurlEndpointResolver.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/LnurlEndpointResolver.kt @@ -24,7 +24,7 @@ package com.vitorpamplona.quartz.nip57Zaps.validate * Resolves an LNURL-pay URL to its [LnurlEndpointInfo]. Implementations should * consult [LnurlEndpointCache] first and only fall through to a network fetch * on a miss. Lives in quartz so that pure-logic verifiers (e.g. - * [LnZapReceiptValidator]) can be wired against an abstract interface without + * [ZapReceiptValidator]) can be wired against an abstract interface without * depending on OkHttp. */ fun interface LnurlEndpointResolver { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/LnZapReceiptValidator.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/ZapReceiptValidator.kt similarity index 95% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/LnZapReceiptValidator.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/ZapReceiptValidator.kt index da42cdb131..b7f812289d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/LnZapReceiptValidator.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/ZapReceiptValidator.kt @@ -45,7 +45,7 @@ import com.vitorpamplona.quartz.utils.BigDecimal * check on the receipt's signer pubkey is non-negotiable when an * `expectedNostrPubkey` is provided. */ -object LnZapReceiptValidator { +object ZapReceiptValidator { sealed class Result { data object Valid : Result() @@ -153,3 +153,9 @@ object LnZapReceiptValidator { ?.get(1) ?.takeIf { it.isNotBlank() } } + +@Deprecated( + "Renamed to ZapReceiptValidator. Validates NIP-57 zap receipts (ZapReceiptEvent).", + ReplaceWith("ZapReceiptValidator", "com.vitorpamplona.quartz.nip57Zaps.validate.ZapReceiptValidator"), +) +typealias LnZapReceiptValidator = ZapReceiptValidator diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/NostrSignerWithClientTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/NostrSignerWithClientTag.kt index 48c72d8de2..3e9cd2ee4b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/NostrSignerWithClientTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/NostrSignerWithClientTag.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent /** @@ -94,7 +94,7 @@ class NostrSignerWithClientTag( fromPublicKey: HexKey, ): String = inner.nip44Decrypt(ciphertext, fromPublicKey) - override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = inner.decryptZapEvent(event) + override suspend fun decryptZapEvent(event: ZapRequestEvent): PrivateZapEvent = inner.decryptZapEvent(event) override suspend fun deriveKey(nonce: HexKey): HexKey = inner.deriveKey(nonce) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt index d852130e87..06f5682286 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt @@ -261,21 +261,20 @@ import com.vitorpamplona.quartz.nip51Lists.appCurationSet.AppCurationSetEvent import com.vitorpamplona.quartz.nip51Lists.articleCurationSet.ArticleCurationSetEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.OldBookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkSet.BookmarkSetEvent import com.vitorpamplona.quartz.nip51Lists.favoriteAlgoFeedsList.FavoriteAlgoFeedsListEvent -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent import com.vitorpamplona.quartz.nip51Lists.gitAuthorList.GitAuthorListEvent import com.vitorpamplona.quartz.nip51Lists.gitRepositoryList.GitRepositoryListEvent import com.vitorpamplona.quartz.nip51Lists.goodWikiAuthorList.GoodWikiAuthorListEvent import com.vitorpamplona.quartz.nip51Lists.goodWikiRelayList.GoodWikiRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.hashtagList.InterestListEvent +import com.vitorpamplona.quartz.nip51Lists.interestList.InterestListEvent import com.vitorpamplona.quartz.nip51Lists.interestSet.InterestSetEvent import com.vitorpamplona.quartz.nip51Lists.kindMuteSet.KindMuteSetEvent -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.BookmarkSetEvent import com.vitorpamplona.quartz.nip51Lists.mediaFollowList.MediaFollowListEvent import com.vitorpamplona.quartz.nip51Lists.mediaStarterPack.MediaStarterPackEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.peopleList.FollowSetEvent import com.vitorpamplona.quartz.nip51Lists.pictureCurationSet.PictureCurationSetEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent @@ -286,6 +285,7 @@ import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent import com.vitorpamplona.quartz.nip51Lists.simpleGroupList.SimpleGroupListEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip51Lists.videoCurationSet.VideoCurationSetEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent @@ -304,7 +304,7 @@ import com.vitorpamplona.quartz.nip54Wiki.WikiMergeAcceptanceEvent import com.vitorpamplona.quartz.nip54Wiki.WikiMergeRequestEvent import com.vitorpamplona.quartz.nip54Wiki.WikiRedirectEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent @@ -725,7 +725,7 @@ class EventFactory { NwcInfoEvent.KIND -> NwcInfoEvent(id, pubKey, createdAt, tags, content, sig) NwcNotificationEvent.KIND -> NwcNotificationEvent(id, pubKey, createdAt, tags, content, sig) NwcNotificationEvent.LEGACY_KIND -> NwcNotificationEvent(id, pubKey, createdAt, tags, content, sig) - LnZapPrivateEvent.KIND -> LnZapPrivateEvent(id, pubKey, createdAt, tags, content, sig) + PrivateZapEvent.KIND -> PrivateZapEvent(id, pubKey, createdAt, tags, content, sig) ZapRequestEvent.KIND -> ZapRequestEvent(id, pubKey, createdAt, tags, content, sig) LongFormContentEvent.KIND -> LongFormContentEvent(id, pubKey, createdAt, tags, content, sig) MarketplaceEvent.KIND -> MarketplaceEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/SoftwareApplicationEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/SoftwareApplicationEventTest.kt index 25990b13ff..6f5166d193 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/SoftwareApplicationEventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/SoftwareApplicationEventTest.kt @@ -207,6 +207,32 @@ class SoftwareApplicationEventTest { assertEquals("notes", template.content) } + @Test + fun searchIndex_includesReleaseNotesOnlyForNip82() { + val nip82 = + ReleaseArtifactSetEvent( + id = "0".repeat(64), + pubKey = "1".repeat(64), + createdAt = 0, + tags = arrayOf(arrayOf("d", "com.example.app@1.0.0"), arrayOf("i", "com.example.app"), arrayOf("version", "1.0.0")), + content = "Fixed the crash on startup", + sig = "", + ) + assertEquals("Fixed the crash on startup", nip82.indexableContent()) + + // A NIP-51 set can carry encrypted private items in content: never index it. + val nip51 = + ReleaseArtifactSetEvent( + id = "0".repeat(64), + pubKey = "1".repeat(64), + createdAt = 0, + tags = arrayOf(arrayOf("d", "some-uuid"), arrayOf("title", "My Release")), + content = "ciphertext?iv=abc", + sig = "", + ) + assertEquals("My Release", nip51.indexableContent()) + } + @Test fun parses_realWorldAmethystEvent() { // Real NIP-82 event published for Amethyst — used as a regression fixture so renderer diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip85TrustedAssertions/ContactCardPetNameTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip85TrustedAssertions/UserAssertionPetNameTest.kt similarity index 99% rename from quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip85TrustedAssertions/ContactCardPetNameTest.kt rename to quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip85TrustedAssertions/UserAssertionPetNameTest.kt index 1f58771e64..fb330c5182 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip85TrustedAssertions/ContactCardPetNameTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip85TrustedAssertions/UserAssertionPetNameTest.kt @@ -32,7 +32,7 @@ import kotlin.test.assertEquals import kotlin.test.assertNull import kotlin.test.assertTrue -class ContactCardPetNameTest { +class UserAssertionPetNameTest { val signer = NostrSignerInternal(KeyPair()) val targetUser = "e88a691e98d9987c964521dff60025f60700378a4879180dcbbb4a5027850411" diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt index 496def0887..9abb9aa952 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryAssemblerTest.kt @@ -980,7 +980,7 @@ class QueryAssemblerTest : BaseDBTest() { } @Test - fun testContactCardDownloadFromTrustedKeys() = + fun testUserAssertionDownloadFromTrustedKeys() = forEachDB { db -> val filter = Filter( diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/BunkerRequestProcessorConcurrencyTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/BunkerRequestProcessorConcurrencyTest.kt index 5226a302ce..0a2bdce894 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/BunkerRequestProcessorConcurrencyTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/BunkerRequestProcessorConcurrencyTest.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.launch @@ -101,7 +101,7 @@ class BunkerRequestProcessorConcurrencyTest { fromPublicKey: HexKey, ) = "" - override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = throw NotImplementedError() + override suspend fun decryptZapEvent(event: ZapRequestEvent): PrivateZapEvent = throw NotImplementedError() override suspend fun deriveKey(nonce: HexKey): HexKey = throw NotImplementedError() diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/BunkerRequestProcessorTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/BunkerRequestProcessorTest.kt index bc5598a518..ea7339b628 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/BunkerRequestProcessorTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/BunkerRequestProcessorTest.kt @@ -40,7 +40,7 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEvent import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseGetRelays import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePong import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePublicKey -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import kotlinx.coroutines.test.runTest import kotlin.test.Test @@ -116,7 +116,7 @@ class BunkerRequestProcessorTest { return "nip44dec:$ciphertext" } - override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = throw NotImplementedError() + override suspend fun decryptZapEvent(event: ZapRequestEvent): PrivateZapEvent = throw NotImplementedError() override suspend fun deriveKey(nonce: HexKey): HexKey = throw NotImplementedError() @@ -371,7 +371,7 @@ class BunkerRequestProcessorTest { fromPublicKey: HexKey, ) = "" - override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = throw NotImplementedError() + override suspend fun decryptZapEvent(event: ZapRequestEvent): PrivateZapEvent = throw NotImplementedError() override suspend fun deriveKey(nonce: HexKey): HexKey = throw NotImplementedError() diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/NostrConnectSignerServiceTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/NostrConnectSignerServiceTest.kt index 0e631bc3e1..db6c888430 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/NostrConnectSignerServiceTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/server/NostrConnectSignerServiceTest.kt @@ -38,7 +38,7 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEvent import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePublicKey import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.utils.RandomInstance import com.vitorpamplona.quartz.utils.TimeUtils @@ -112,7 +112,7 @@ class NostrConnectSignerServiceTest { fromPublicKey: HexKey, ) = ciphertext - override suspend fun decryptZapEvent(event: ZapRequestEvent): LnZapPrivateEvent = throw NotImplementedError() + override suspend fun decryptZapEvent(event: ZapRequestEvent): PrivateZapEvent = throw NotImplementedError() override suspend fun deriveKey(nonce: HexKey): HexKey = throw NotImplementedError() diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcRequestNip44EventTest.kt similarity index 98% rename from quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt rename to quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcRequestNip44EventTest.kt index edc3143ba5..1a6c7ae24f 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcRequestNip44EventTest.kt @@ -32,7 +32,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs -class LnZapPaymentRequestNip44EventTest { +class NwcRequestNip44EventTest { @Test fun testCreateRequestWithNip44() = runTest { diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractorTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractorTest.kt index 5d044ecb52..51b77ca13f 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractorTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractorTest.kt @@ -153,7 +153,7 @@ class SearchFieldExtractorTest { } @Test - fun contactCardsDecomposeIntoPetnameSummaryAndTopics() { + fun userAssertionsDecomposeIntoPetnameSummaryAndTopics() { // A provider's petname for a person is that provider's NAME for them, // so it lands where kind 0's name does. topics() reads `t` tags, so // the tiers() funnel carries them once, as hashtags. @@ -177,7 +177,7 @@ class SearchFieldExtractorTest { } @Test - fun contactCardsCarryTopicsEvenWithNoPublicPetname() { + fun userAssertionsCarryTopicsEvenWithNoPublicPetname() { // THE SHAPE THIS LIBRARY ITSELF PUBLISHES: build() puts petname and // summary in the NIP-44 content, so a card's only public text is its // topics. They must still reach the backend -- through the hashtag @@ -189,7 +189,7 @@ class SearchFieldExtractorTest { } @Test - fun contactCardsWithNoPublicTextExtractNothing() { + fun userAssertionsWithNoPublicTextExtractNothing() { // The petname and summary of a private card live in the NIP-44 // encrypted content, which is never indexed -- so a card carrying only // scores has nothing to search. diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/RelayListPublicEntriesTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/RelayListPublicEntriesTest.kt index 5111ca4c15..484ecb6f2f 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/RelayListPublicEntriesTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip51Lists/relayLists/RelayListPublicEntriesTest.kt @@ -60,7 +60,7 @@ class RelayListPublicEntriesTest { ) @Test - fun relayFeedsListKeepsTheSurvivorPublic() = + fun favoriteRelayListKeepsTheSurvivorPublic() = runTest { val before = publicListFromAnotherClient<FavoriteRelayListEvent>(FavoriteRelayListEvent.KIND) val after = FavoriteRelayListEvent.updateRelayList(before, listOf(damus), signer, 1740669817) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestAnonTagTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/ZapRequestAnonTagTest.kt similarity index 99% rename from quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestAnonTagTest.kt rename to quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/ZapRequestAnonTagTest.kt index 9592029d3c..8bf0e14306 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestAnonTagTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/ZapRequestAnonTagTest.kt @@ -37,7 +37,7 @@ import kotlin.test.assertTrue * (encrypted `anon`) requests use throwaway keys — tagging anything derived * from them is either useless or, for the decrypted sender, a doxxing risk. */ -class LnZapRequestAnonTagTest { +class ZapRequestAnonTagTest { private val signer = NostrSignerInternal( "nsec10g0wheggqn9dawlc0yuv6adnat6n09anr7eyykevw2dm8xa5fffs0wsdsr".nsecToKeyPair(), diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestGroupTagTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/ZapRequestGroupTagTest.kt similarity index 99% rename from quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestGroupTagTest.kt rename to quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/ZapRequestGroupTagTest.kt index 07451237b4..14bce2e8e9 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestGroupTagTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/ZapRequestGroupTagTest.kt @@ -39,7 +39,7 @@ import kotlin.test.assertTrue * their rooms) never matches it — so a zap on a group message would be invisible exactly where it was * meant to be seen. Reactions already copy the tag (`ReactionAction`); this pins the same rule for zaps. */ -class LnZapRequestGroupTagTest { +class ZapRequestGroupTagTest { private val signer = DeterministicSigner( "nsec10g0wheggqn9dawlc0yuv6adnat6n09anr7eyykevw2dm8xa5fffs0wsdsr".nsecToKeyPair(), diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestKindTagTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/ZapRequestKindTagTest.kt similarity index 99% rename from quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestKindTagTest.kt rename to quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/ZapRequestKindTagTest.kt index 1ba8c3c279..38e125424d 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestKindTagTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/ZapRequestKindTagTest.kt @@ -32,7 +32,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -class LnZapRequestKindTagTest { +class ZapRequestKindTagTest { private val signer = DeterministicSigner( "nsec10g0wheggqn9dawlc0yuv6adnat6n09anr7eyykevw2dm8xa5fffs0wsdsr".nsecToKeyPair(), diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/LnZapReceiptValidatorTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/ZapReceiptValidatorTest.kt similarity index 70% rename from quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/LnZapReceiptValidatorTest.kt rename to quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/ZapReceiptValidatorTest.kt index ef3344b0b0..5487d552e0 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/LnZapReceiptValidatorTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip57Zaps/validate/ZapReceiptValidatorTest.kt @@ -26,7 +26,7 @@ import kotlin.test.assertEquals import kotlin.test.assertIs import kotlin.test.assertTrue -class LnZapReceiptValidatorTest { +class ZapReceiptValidatorTest { // Mainnet 100,000 sat invoice (= 100_000_000 msats). From LnInvoiceUtilTest. private val invoice100kSats = "lnbc1m1pjt9u0qsp553q90pj5mafzv20w45eqavned9tgwhl4q99n9s5ppcw24nzw3zeqpp5002kd3ktym67du86kj665fgaev7ka8ys7j5yz5fg686lr5e2gfkshp5dkk27nnuax05az3pk2r6ytxtvwn5j4xzsq9ajprhc7crjkmgvr3qxqyjw5qcqpjrzjqtzxvfsuxe4l92pf97tt4rcgpy2xalkmlwexh899wqxf83l8nwv4xzh0gvqq89qqqqqqqqlgqqqqq0gqvs9qxpqysgqx5mz04wd7kqu5zhhel9enr036hjrp4gga0nz084p2asjl36a0zmrk6mhqa249zsgqref2rlvhffm73u7rxgr47gden6rugup4ksvpzsqvds4pz" @@ -80,99 +80,99 @@ class LnZapReceiptValidatorTest { @Test fun `valid receipt passes all checks`() { - val result = LnZapReceiptValidator.validate(receipt(), providerPubkey, lnurlOfRecipient) - assertEquals(LnZapReceiptValidator.Result.Valid, result) + val result = ZapReceiptValidator.validate(receipt(), providerPubkey, lnurlOfRecipient) + assertEquals(ZapReceiptValidator.Result.Valid, result) } @Test fun `missing zap request fails with MISSING_ZAP_REQUEST`() { val r = receipt(description = null) - val result = LnZapReceiptValidator.validate(r, providerPubkey, lnurlOfRecipient) - val invalid = assertIs<LnZapReceiptValidator.Result.Invalid>(result) - assertEquals(LnZapReceiptValidator.Result.Reason.MISSING_ZAP_REQUEST, invalid.reason) + val result = ZapReceiptValidator.validate(r, providerPubkey, lnurlOfRecipient) + val invalid = assertIs<ZapReceiptValidator.Result.Invalid>(result) + assertEquals(ZapReceiptValidator.Result.Reason.MISSING_ZAP_REQUEST, invalid.reason) } @Test fun `wrong signer fails with MISMATCHED_NOSTR_PUBKEY`() { val attackerPubkey = "1".repeat(64) - val result = LnZapReceiptValidator.validate(receipt(signerPubkey = attackerPubkey), providerPubkey, lnurlOfRecipient) - val invalid = assertIs<LnZapReceiptValidator.Result.Invalid>(result) - assertEquals(LnZapReceiptValidator.Result.Reason.MISMATCHED_NOSTR_PUBKEY, invalid.reason) + val result = ZapReceiptValidator.validate(receipt(signerPubkey = attackerPubkey), providerPubkey, lnurlOfRecipient) + val invalid = assertIs<ZapReceiptValidator.Result.Invalid>(result) + assertEquals(ZapReceiptValidator.Result.Reason.MISMATCHED_NOSTR_PUBKEY, invalid.reason) } @Test fun `null expected pubkey skips the check`() { val attackerPubkey = "1".repeat(64) - val result = LnZapReceiptValidator.validate(receipt(signerPubkey = attackerPubkey), null, lnurlOfRecipient) - assertEquals(LnZapReceiptValidator.Result.Valid, result) + val result = ZapReceiptValidator.validate(receipt(signerPubkey = attackerPubkey), null, lnurlOfRecipient) + assertEquals(ZapReceiptValidator.Result.Valid, result) } @Test fun `missing bolt11 fails with MISSING_OR_BAD_BOLT11`() { - val result = LnZapReceiptValidator.validate(receipt(bolt11 = null), providerPubkey, lnurlOfRecipient) - val invalid = assertIs<LnZapReceiptValidator.Result.Invalid>(result) - assertEquals(LnZapReceiptValidator.Result.Reason.MISSING_OR_BAD_BOLT11, invalid.reason) + val result = ZapReceiptValidator.validate(receipt(bolt11 = null), providerPubkey, lnurlOfRecipient) + val invalid = assertIs<ZapReceiptValidator.Result.Invalid>(result) + assertEquals(ZapReceiptValidator.Result.Reason.MISSING_OR_BAD_BOLT11, invalid.reason) } @Test fun `mismatched amount fails with MISMATCHED_AMOUNT`() { // bolt11 is 100,000,000 msats; request claims 50,000,000. val r = receipt(description = zapRequestJson(amountMillisats = 50_000_000L)) - val result = LnZapReceiptValidator.validate(r, providerPubkey, lnurlOfRecipient) - val invalid = assertIs<LnZapReceiptValidator.Result.Invalid>(result) - assertEquals(LnZapReceiptValidator.Result.Reason.MISMATCHED_AMOUNT, invalid.reason) + val result = ZapReceiptValidator.validate(r, providerPubkey, lnurlOfRecipient) + val invalid = assertIs<ZapReceiptValidator.Result.Invalid>(result) + assertEquals(ZapReceiptValidator.Result.Reason.MISMATCHED_AMOUNT, invalid.reason) } @Test fun `missing amount tag is accepted when not strict`() { val r = receipt(description = zapRequestJson(amountMillisats = null)) - val result = LnZapReceiptValidator.validate(r, providerPubkey, lnurlOfRecipient, strictAmount = false) - assertEquals(LnZapReceiptValidator.Result.Valid, result) + val result = ZapReceiptValidator.validate(r, providerPubkey, lnurlOfRecipient, strictAmount = false) + assertEquals(ZapReceiptValidator.Result.Valid, result) } @Test fun `missing amount tag fails when strict`() { val r = receipt(description = zapRequestJson(amountMillisats = null)) - val result = LnZapReceiptValidator.validate(r, providerPubkey, lnurlOfRecipient, strictAmount = true) - val invalid = assertIs<LnZapReceiptValidator.Result.Invalid>(result) - assertEquals(LnZapReceiptValidator.Result.Reason.MISMATCHED_AMOUNT, invalid.reason) + val result = ZapReceiptValidator.validate(r, providerPubkey, lnurlOfRecipient, strictAmount = true) + val invalid = assertIs<ZapReceiptValidator.Result.Invalid>(result) + assertEquals(ZapReceiptValidator.Result.Reason.MISMATCHED_AMOUNT, invalid.reason) } @Test fun `mismatched lnurl fails with MISMATCHED_LNURL`() { val r = receipt(description = zapRequestJson(lnurl = "other@example.org")) - val result = LnZapReceiptValidator.validate(r, providerPubkey, lnurlOfRecipient) - val invalid = assertIs<LnZapReceiptValidator.Result.Invalid>(result) - assertEquals(LnZapReceiptValidator.Result.Reason.MISMATCHED_LNURL, invalid.reason) + val result = ZapReceiptValidator.validate(r, providerPubkey, lnurlOfRecipient) + val invalid = assertIs<ZapReceiptValidator.Result.Invalid>(result) + assertEquals(ZapReceiptValidator.Result.Reason.MISMATCHED_LNURL, invalid.reason) } @Test fun `missing lnurl tag is accepted`() { val r = receipt(description = zapRequestJson(lnurl = null)) - val result = LnZapReceiptValidator.validate(r, providerPubkey, lnurlOfRecipient) - assertEquals(LnZapReceiptValidator.Result.Valid, result) + val result = ZapReceiptValidator.validate(r, providerPubkey, lnurlOfRecipient) + assertEquals(ZapReceiptValidator.Result.Valid, result) } @Test fun `lnurl tag in URL form matches lud16 expectation`() { val r = receipt(description = zapRequestJson(lnurl = lnurlUrl)) - val result = LnZapReceiptValidator.validate(r, providerPubkey, lnurlOfRecipient) - assertEquals(LnZapReceiptValidator.Result.Valid, result) + val result = ZapReceiptValidator.validate(r, providerPubkey, lnurlOfRecipient) + assertEquals(ZapReceiptValidator.Result.Valid, result) } @Test fun `lnurl tag in bech32 form matches lud16 expectation`() { val bech32 = LnurlForm.urlToBech32(lnurlUrl) val r = receipt(description = zapRequestJson(lnurl = bech32)) - val result = LnZapReceiptValidator.validate(r, providerPubkey, lnurlOfRecipient) - assertEquals(LnZapReceiptValidator.Result.Valid, result) + val result = ZapReceiptValidator.validate(r, providerPubkey, lnurlOfRecipient) + assertEquals(ZapReceiptValidator.Result.Valid, result) } @Test fun `pubkey check is case-insensitive`() { val r = receipt(signerPubkey = providerPubkey.uppercase()) - val result = LnZapReceiptValidator.validate(r, providerPubkey, lnurlOfRecipient) - assertEquals(LnZapReceiptValidator.Result.Valid, result) + val result = ZapReceiptValidator.validate(r, providerPubkey, lnurlOfRecipient) + assertEquals(ZapReceiptValidator.Result.Valid, result) } } diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapperTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapperTest.kt index 76022f4303..6460a74a65 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapperTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapperTest.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.nip01Core.jackson import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor import kotlin.test.Test import kotlin.test.assertContentEquals diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt index a3ee938277..f33431cd8f 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt @@ -39,7 +39,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse -import com.vitorpamplona.quartz.nip51Lists.followList.StarterPackEvent +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor import kotlin.test.Test import kotlin.test.assertContentEquals From 08169f8497690818c8817f221d1471f353ea8bc5 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 21:12:49 +0000 Subject: [PATCH 14/60] refactor(commons): move the napplet wire protocol and scheduled posts to commonMain Tier 4 of the commons migration sweep: two jvmAndroid groups that were each pinned by a single JVM API. Napplet: NappletProtocolJson (and NappletIdentityWatch, NappletRequestRouter, which only waited on it) used java.util.Base64 and System.currentTimeMillis. Now kotlin.io.encoding.Base64 and TimeUtils.now(). The decoder uses PaddingOption.PRESENT_OPTIONAL because java.util.Base64.getDecoder() accepted unpadded input; a test pins that. Scheduled posts: ScheduledPostStore was pinned by java.io.File and a POSIX chmod. It now takes an okio Path + FileSystem (plus a String constructor, the FileSystemNip95BlobStore shape, so callers need no okio). Owner-only permissions are a new expect fun restrictFileToOwner: Files.setPosix... on jvmAndroid, chmod(0600) on iOS. The temp-file swap is FileSystem.atomicMove, which replaces an existing target everywhere, so the rename/delete/rename fallback is gone. The work gate, publisher and notifier came along. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjQQN9CgWHVtNCSWqnKoqY --- .../com/vitorpamplona/amethyst/AppModules.kt | 2 +- .../napplet/NappletProtocolJsonTest.kt | 7 ++ .../ScheduledPostWorkGateTest.kt | 2 +- .../commons/napplet/NappletIdentityWatch.kt | 0 .../commons/napplet/NappletRequestRouter.kt | 0 .../napplet/protocol/NappletProtocolJson.kt | 20 +++-- .../scheduledposts/ScheduledPostNotifier.kt | 0 .../scheduledposts/ScheduledPostPublisher.kt | 0 .../scheduledposts/ScheduledPostStore.kt | 78 +++++++++---------- .../scheduledposts/ScheduledPostWorkGate.kt | 0 .../amethyst/commons/util/OwnerOnlyFile.kt | 34 ++++++++ .../commons/util/OwnerOnlyFile.ios.kt | 40 ++++++++++ .../commons/util/OwnerOnlyFile.jvmAndroid.kt | 42 ++++++++++ .../scheduledposts/ScheduledPostStoreTest.kt | 2 +- .../DesktopScheduledPostStore.kt | 2 +- .../HeadlessScheduledPublish.kt | 2 +- 16 files changed, 175 insertions(+), 56 deletions(-) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletIdentityWatch.kt (100%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletRequestRouter.kt (100%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/napplet/protocol/NappletProtocolJson.kt (97%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostNotifier.kt (100%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostPublisher.kt (100%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostStore.kt (88%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostWorkGate.kt (100%) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/OwnerOnlyFile.kt create mode 100644 commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/OwnerOnlyFile.ios.kt create mode 100644 commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/OwnerOnlyFile.jvmAndroid.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt index 3e0c5f6a6e..934226f2c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -1154,7 +1154,7 @@ class AppModules( // Local store for posts the user has scheduled to publish later. Backed by a // single JSON file under the app's private filesDir; read by ScheduledPostWorker. val scheduledPostStore = - ScheduledPostStore(File(appContext.filesDir, ScheduledPostStore.FILE_NAME)) + ScheduledPostStore(File(appContext.filesDir, ScheduledPostStore.FILE_NAME).path) // Organizes cache clearing val trimmingService by diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/napplet/NappletProtocolJsonTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/napplet/NappletProtocolJsonTest.kt index 1f2422de4b..93399e85a2 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/napplet/NappletProtocolJsonTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/napplet/NappletProtocolJsonTest.kt @@ -184,6 +184,13 @@ class NappletProtocolJsonTest { assertEquals("Hi", up.bytes.decodeToString()) } + @Test + fun uploadAcceptsUnpaddedBase64() { + // java.util.Base64 decoded "SGk" (no '=') as "Hi"; the commonMain decoder must too. + val up = NappletProtocolJson.decodeRequest("""{"type":"upload.upload","request":{"dataBase64":"SGk"}}""") as NappletRequest.UploadBlob + assertEquals("Hi", up.bytes.decodeToString()) + } + @Test fun unknownTypeDecodesToNull() { assertNull(NappletProtocolJson.decodeRequest("""{"type":"inc.emit","id":"1"}""")) diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/scheduledposts/ScheduledPostWorkGateTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/scheduledposts/ScheduledPostWorkGateTest.kt index e346274630..d3028575ea 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/scheduledposts/ScheduledPostWorkGateTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/scheduledposts/ScheduledPostWorkGateTest.kt @@ -57,7 +57,7 @@ class ScheduledPostWorkGateTest { decisions.clear() } - private fun newStore() = ScheduledPostStore(file) + private fun newStore() = ScheduledPostStore(file.path) private fun TestScope.startGate(store: ScheduledPostStore) = ScheduledPostWorkGate( diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletIdentityWatch.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletIdentityWatch.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletIdentityWatch.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletIdentityWatch.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletRequestRouter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletRequestRouter.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletRequestRouter.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletRequestRouter.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/napplet/protocol/NappletProtocolJson.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/protocol/NappletProtocolJson.kt similarity index 97% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/napplet/protocol/NappletProtocolJson.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/protocol/NappletProtocolJson.kt index 027cae99df..af171f6188 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/napplet/protocol/NappletProtocolJson.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/protocol/NappletProtocolJson.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.commons.napplet.protocol import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonObject @@ -37,7 +38,7 @@ import kotlinx.serialization.json.long import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonArray import kotlinx.serialization.json.putJsonObject -import java.util.Base64 +import kotlin.io.encoding.Base64 /** * Marshals the KMP-pure [NappletRequest] / [NappletResponse] types to and from the wire the @@ -45,10 +46,10 @@ import java.util.Base64 * (`@napplet/shim` / `@napplet/nap`): requests are `{ "type": "<domain>.<action>", "id", ...fields }` * and replies are `{ "type": "<domain>.<action>.result", "id", ...fields }`. * - * Lives in `commons/jvmAndroid` (not in any single front end) because the wire contract is identical + * Lives in `commons/commonMain` (not in any single front end) because the wire contract is identical * across hosts: the Android `:napplet` WebView host and a future desktop host both marshal through - * here. It depends only on `quartz` (Event/Filter), kotlinx.serialization, and `java.util.Base64` - * (available on Android API 26+ and the JVM) — no platform-UI or process APIs. + * here. It depends only on `quartz` (Event/Filter), kotlinx.serialization and `kotlin.io.encoding` + * — no platform-UI or process APIs. * * This is the only place the boundary parses untrusted applet input, so it is deliberately * strict: an unrecognized `type` decodes to `null` (the broker denies it) and a malformed/short @@ -58,6 +59,9 @@ import java.util.Base64 object NappletProtocolJson { private val json = Json { ignoreUnknownKeys = true } + // Accepts unpadded input, as java.util.Base64's decoder did before this moved to commonMain. + private val lenientBase64 = Base64.withPadding(Base64.PaddingOption.PRESENT_OPTIONAL) + /** The `type` discriminant of a request envelope, used to build the matching `.result` type. */ fun readType(envelopeJson: String): String? = json.parseToJsonElement(envelopeJson).jsonObject.str("type") @@ -166,7 +170,7 @@ object NappletProtocolJson { kind = t.kindOf(), tags = decodeTags(t), content = t.str("content") ?: "", - createdAt = t["created_at"]?.jsonPrimitive?.long ?: (System.currentTimeMillis() / 1000), + createdAt = t["created_at"]?.jsonPrimitive?.long ?: TimeUtils.now(), ) } // NIP-07 nip44.encrypt/decrypt: crypto only, no publish. `peer` is the counterparty @@ -194,7 +198,7 @@ object NappletProtocolJson { // The Blob in `request.data` is inlined as base64 `request.dataBase64` by shell.html. val request = o.getValue("request").jsonObject NappletRequest.UploadBlob( - bytes = Base64.getDecoder().decode(request.req("dataBase64")), + bytes = lenientBase64.decode(request.req("dataBase64")), contentType = request.str("mimeType") ?: "application/octet-stream", filename = request.str("filename"), ) @@ -275,7 +279,7 @@ object NappletProtocolJson { } is NappletResponse.Bytes -> { put("ok", true) - put("bytes", Base64.getEncoder().encodeToString(response.bytes)) + put("bytes", Base64.encode(response.bytes)) put("mime", response.contentType) } is NappletResponse.ResourceInfo -> { @@ -301,7 +305,7 @@ object NappletProtocolJson { put("url", item.url) put("ok", item.resource != null) item.resource?.let { - put("bytes", Base64.getEncoder().encodeToString(it.bytes)) + put("bytes", Base64.encode(it.bytes)) put("mime", it.contentType) } item.error?.let { put("error", it) } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostNotifier.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostNotifier.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostNotifier.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostNotifier.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostPublisher.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostPublisher.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostPublisher.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostPublisher.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostStore.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostStore.kt similarity index 88% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostStore.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostStore.kt index d19e8e7a6c..ec34e46725 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostStore.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostStore.kt @@ -20,21 +20,31 @@ */ package com.vitorpamplona.amethyst.commons.scheduledposts +import com.vitorpamplona.amethyst.commons.util.platformFileSystem +import com.vitorpamplona.amethyst.commons.util.restrictFileToOwner import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.serialization.json.Json -import java.io.File -import java.nio.file.Files -import java.nio.file.attribute.PosixFilePermission +import okio.FileSystem +import okio.Path +import okio.Path.Companion.toPath class ScheduledPostStore( - private val storageFile: File, - private val nowSec: () -> Long = { System.currentTimeMillis() / 1000 }, + private val storageFile: Path, + private val nowSec: () -> Long = { TimeUtils.now() }, + private val fileSystem: FileSystem = platformFileSystem, ) { + /** Takes the platform's own path string, so callers need no okio of their own. */ + constructor( + storagePath: String, + nowSec: () -> Long = { TimeUtils.now() }, + ) : this(storagePath.toPath(), nowSec) + /** * `encodeDefaults = true` so this writes the same bytes Jackson did — kotlinx * omits a value equal to its default, which would silently drop `"version":1` @@ -214,7 +224,7 @@ class ScheduledPostStore( */ suspend fun publishNow( id: String, - nowSec: Long = System.currentTimeMillis() / 1000, + nowSec: Long = TimeUtils.now(), ): Boolean = mutex.withLock { ensureLoaded() @@ -336,8 +346,8 @@ class ScheduledPostStore( private fun reloadFromDiskLocked() { val fromDisk = try { - if (storageFile.exists() && storageFile.length() > 0) { - json.decodeFromString<ScheduledPostFile>(storageFile.readText()).posts.toMutableList() + if (hasContent()) { + json.decodeFromString<ScheduledPostFile>(readText()).posts.toMutableList() } else { // File vanished — treat as no external state; keep current in-memory. return @@ -354,8 +364,8 @@ class ScheduledPostStore( if (loaded) return posts = try { - if (storageFile.exists() && storageFile.length() > 0) { - json.decodeFromString<ScheduledPostFile>(storageFile.readText()).posts.toMutableList() + if (hasContent()) { + json.decodeFromString<ScheduledPostFile>(readText()).posts.toMutableList() } else { mutableListOf() } @@ -367,7 +377,7 @@ class ScheduledPostStore( // Secure a pre-existing file up front (an older build may have left it at the // 0644 umask default) so it's owner-only even if nothing mutates the store // this session. - if (storageFile.exists()) restrictToOwner(storageFile) + if (fileSystem.exists(storageFile)) restrictFileToOwner(storageFile, TAG) var dirty = purgeStale(nowSec()) // Recover claims stranded by a crash-mid-publish so they aren't lost forever. if (recoverStuckClaimsLocked(nowSec())) dirty = true @@ -412,48 +422,30 @@ class ScheduledPostStore( private fun persist() { val snapshot = posts.toList() _flow.value = snapshot - storageFile.parentFile?.mkdirs() - val tmp = File(storageFile.parentFile, storageFile.name + ".tmp") + val tmp = "$storageFile.tmp".toPath() try { - tmp.writeText(json.encodeToString(ScheduledPostFile(version = 1, posts = snapshot))) + storageFile.parent?.let { fileSystem.createDirectories(it) } + fileSystem.write(tmp) { writeUtf8(json.encodeToString(ScheduledPostFile(version = 1, posts = snapshot))) } // Restrict to owner-only BEFORE the rename so the store is never briefly // world-readable. It holds pre-signed events + the account's pubkey, which // must not leak to other local users on a shared machine. - restrictToOwner(tmp) - if (!tmp.renameTo(storageFile)) { - if (!storageFile.delete()) { - Log.w(TAG) { "Failed to delete existing $storageFile before rename retry" } - } - if (!tmp.renameTo(storageFile)) { - Log.e(TAG) { "Failed to rename $tmp to $storageFile" } - if (!tmp.delete()) { - Log.w(TAG) { "Failed to clean up temp file $tmp after rename failure" } - } - } - } + restrictFileToOwner(tmp, TAG) + // Replaces an existing target, including on Windows, where a plain rename + // onto an existing file fails. + fileSystem.atomicMove(tmp, storageFile) } catch (e: Exception) { Log.e(TAG, "Failed to persist scheduled posts to $storageFile", e) - if (!tmp.delete()) { - Log.w(TAG) { "Failed to clean up temp file $tmp after persist exception" } + try { + fileSystem.delete(tmp) + } catch (e: Exception) { + Log.w(TAG) { "Failed to clean up temp file $tmp after persist exception: ${e.message}" } } } } - /** - * Best-effort chmod to `0600` (owner read/write only). No-op on filesystems - * without POSIX permissions (e.g. Windows), where confidentiality relies on the - * per-user home directory instead. Never throws. - */ - private fun restrictToOwner(file: File) { - try { - Files.setPosixFilePermissions( - file.toPath(), - setOf(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE), - ) - } catch (e: Exception) { - Log.w(TAG) { "Could not restrict permissions on $file: ${e.message}" } - } - } + private fun hasContent(): Boolean = (fileSystem.metadataOrNull(storageFile)?.size ?: 0L) > 0L + + private fun readText(): String = fileSystem.read(storageFile) { readUtf8() } companion object { private const val TAG = "ScheduledPostStore" diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostWorkGate.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostWorkGate.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostWorkGate.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostWorkGate.kt diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/OwnerOnlyFile.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/OwnerOnlyFile.kt new file mode 100644 index 0000000000..05934c55ca --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/OwnerOnlyFile.kt @@ -0,0 +1,34 @@ +/* + * 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.commons.util + +import okio.Path + +/** + * Restricts the file at [path] to owner read/write (`0600`), best-effort. Never throws. + * + * Silent where the filesystem has no POSIX permissions (Windows), where the user profile's + * ACLs apply instead; logs under [tag] when a POSIX filesystem refuses. + */ +expect fun restrictFileToOwner( + path: Path, + tag: String, +) diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/OwnerOnlyFile.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/OwnerOnlyFile.ios.kt new file mode 100644 index 0000000000..42b3c0c516 --- /dev/null +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/OwnerOnlyFile.ios.kt @@ -0,0 +1,40 @@ +/* + * 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.commons.util + +import com.vitorpamplona.quartz.utils.Log +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.convert +import okio.Path +import platform.posix.S_IRUSR +import platform.posix.S_IWUSR +import platform.posix.chmod +import platform.posix.errno + +@OptIn(ExperimentalForeignApi::class) +actual fun restrictFileToOwner( + path: Path, + tag: String, +) { + if (chmod(path.toString(), (S_IRUSR or S_IWUSR).convert()) != 0) { + Log.w(tag) { "Could not restrict permissions on $path: errno $errno" } + } +} diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/OwnerOnlyFile.jvmAndroid.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/OwnerOnlyFile.jvmAndroid.kt new file mode 100644 index 0000000000..5d4d16c3b7 --- /dev/null +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/OwnerOnlyFile.jvmAndroid.kt @@ -0,0 +1,42 @@ +/* + * 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.commons.util + +import com.vitorpamplona.quartz.utils.Log +import okio.Path +import java.nio.file.Files +import java.nio.file.attribute.PosixFilePermission + +actual fun restrictFileToOwner( + path: Path, + tag: String, +) { + try { + Files.setPosixFilePermissions( + path.toNioPath(), + setOf(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE), + ) + } catch (_: UnsupportedOperationException) { + // Windows: no POSIX permissions; the user profile's NTFS ACLs apply instead. + } catch (e: Exception) { + Log.w(tag) { "Could not restrict permissions on $path: ${e.message}" } + } +} diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostStoreTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostStoreTest.kt index 6033715624..bf0fdb92e2 100644 --- a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostStoreTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostStoreTest.kt @@ -47,7 +47,7 @@ class ScheduledPostStoreTest { file = File(temp.root, "scheduled_posts.json") } - private fun newStore(now: () -> Long = { System.currentTimeMillis() / 1000 }) = ScheduledPostStore(file, now) + private fun newStore(now: () -> Long = { System.currentTimeMillis() / 1000 }) = ScheduledPostStore(file.path, now) private fun samplePost( id: String = "id-1", diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/scheduledposts/DesktopScheduledPostStore.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/scheduledposts/DesktopScheduledPostStore.kt index ebd05606b5..96ccc66328 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/scheduledposts/DesktopScheduledPostStore.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/scheduledposts/DesktopScheduledPostStore.kt @@ -52,7 +52,7 @@ object DesktopScheduledPostStore { if (file.exists()) { setOwnerOnly(file, PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE) } - return ScheduledPostStore(file) + return ScheduledPostStore(file.path) } private fun setDirPermissions(dir: File) = diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/scheduledposts/HeadlessScheduledPublish.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/scheduledposts/HeadlessScheduledPublish.kt index 9c5fb8ee77..49b554ba66 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/scheduledposts/HeadlessScheduledPublish.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/scheduledposts/HeadlessScheduledPublish.kt @@ -82,7 +82,7 @@ fun runHeadlessPublish(): Int { } private fun headlessDrain(log: (String) -> Unit): Int { - val store = ScheduledPostStore(File(scheduledDir(), ScheduledPostStore.FILE_NAME)) + val store = ScheduledPostStore(File(scheduledDir(), ScheduledPostStore.FILE_NAME).path) // Build a plain OkHttp client for websockets — no Tor, no proxy. Headless mode // reads no settings, so we cannot know Tor preferences; scheduled posts that From 150177f5a0220bf6e84d23e6f839186cc989f8b4 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 21:13:01 +0000 Subject: [PATCH 15/60] fix(strings): move the cordn and backup strings to commonsUI, and write the rule down The 2026-09-22 move left Android res/ with only the ~190 strings a platform API reads synchronously. The cordn UI and the backup-conflict review then added 447 keys back to it. All of them move with tools/strings-migrate (1,131 elements across the locales that translate them), and the 18 files that read them now use Res.string/Res.plurals. The parts that were not a rename: - stringRes(context, ...) in the suspend attachment/voice senders becomes loadStringRes; adminFailureText becomes suspend (its only caller is already inside scope.launch). - The backup review held labels as @StringRes Int (DiffGroup.label, the ReviewRow *Res fields, eventTypeName/Explainer, the tile/hero/lane params). They are StringResource / PluralStringResource now. - rememberPresentation resolved labels inside remember {} through LocalResources. Compose resources have no blocking accessor, so the 16 labels presentationOf writes into item text are resolved in composition into a map, and that map is the remember key. - androidx pluralStringResource -> the compose-resources one. The rule is now in .claude/CLAUDE.md ("Strings"), commonsUI/ARCHITECTURE.md and the android-expert and find-missing-translations skills: new strings go in commonsUI composeResources, Android-only screens included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjQQN9CgWHVtNCSWqnKoqY --- .claude/CLAUDE.md | 42 ++ .claude/skills/android-expert/SKILL.md | 21 +- .../skills/find-missing-translations/SKILL.md | 7 + .../loggedIn/backups/BackupConflictCards.kt | 131 ++-- .../backups/BackupConflictEventViews.kt | 197 +++--- .../backups/BackupConflictListViews.kt | 156 +++-- .../backups/BackupConflictPresentation.kt | 328 ++++++---- .../backups/BackupConflictReviewScreen.kt | 86 ++- .../chats/cordnGroup/CordnComposer.kt | 5 +- .../cordnGroup/CordnCreateGroupScreen.kt | 149 +++-- .../cordnGroup/CordnCreateMembersScreen.kt | 58 +- .../chats/cordnGroup/CordnGroupChatScreen.kt | 78 ++- .../chats/cordnGroup/CordnGroupInfoScreen.kt | 161 +++-- .../chats/cordnGroup/CordnGroupListScreen.kt | 26 +- .../cordnGroup/CordnInvitationsScreen.kt | 51 +- .../chats/cordnGroup/CordnMessageRow.kt | 61 +- .../settings/cordn/CordnBackupScreen.kt | 45 +- .../settings/cordn/CordnCoordinatorsScreen.kt | 114 ++-- .../settings/cordn/CordnKeyPackagesScreen.kt | 50 +- .../settings/cordn/CordnLinkScreen.kt | 16 +- .../settings/cordn/CordnMigrateScreen.kt | 7 +- .../src/main/res/values-hi-rIN/strings.xml | 318 ---------- .../src/main/res/values-hu-rHU/strings.xml | 318 ---------- .../src/main/res/values-pl-rPL/strings.xml | 378 ------------ amethyst/src/main/res/values/strings.xml | 570 ------------------ commonsUI/ARCHITECTURE.md | 5 +- .../values-hi-rIN/strings.xml | 318 ++++++++++ .../values-hu-rHU/strings.xml | 318 ++++++++++ .../values-pl-rPL/strings.xml | 378 ++++++++++++ .../composeResources/values/strings.xml | 570 ++++++++++++++++++ 30 files changed, 2777 insertions(+), 2185 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 6038af56e7..886a0dfe3e 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -285,6 +285,48 @@ Quartz uses expect/actual for platform-specific implementations (e.g. crypto backed by `secp256k1-kmp-jni-android` on Android and `secp256k1-kmp-jni-jvm` on JVM). See `/kotlin-multiplatform` for the expect/actual and source-set patterns. +## Strings + +**MANDATORY: every new user-visible string goes in `commonsUI` Compose resources**, +`commonsUI/src/commonMain/composeResources/values/strings.xml` — **never** in +`amethyst/src/main/res/values/strings.xml`, even for an Android-only screen. A +screen cannot move to `commonsUI` (or be rendered by Desktop) while its labels +live in Android `res/`, and every key added there has to be migrated back out +later. (The 2026-09-22 move emptied Android `res/` down to ~190 keys; the next two +features put 447 back.) + +- Reference it as `Res.string.my_key` / `Res.plurals.my_key`, importing + `com.vitorpamplona.amethyst.commons.resources.Res` **and** the per-key accessor + `com.vitorpamplona.amethyst.commons.resources.my_key` (Compose resources generates + each key as an extension property). +- Read it with `stringRes(Res.string.x)` / `pluralStringRes(Res.plurals.x, n, …)` + in composition — the app-side `ui.stringRes` overloads delegate to the + commonsUI bridge (`commons/ui/StringRes.kt`), so one import serves both kinds. +- Outside composition there is **no blocking accessor**: use `loadStringRes(…)` + from a coroutine (or make the function `suspend`), or resolve the label in + composition and pass it down as a `String`. A `runBlocking` bridge is ruled out. +- Hold a string reference as `StringResource` / `PluralStringResource`, not a + `@StringRes Int`. +- Format arguments must be positional (`%1$s`, `%2$d`) — compose-resources does + not format bare `%s`/`%d`. +- Escaping is not Android's: write `'` and `"` bare (no `\'`), and never wrap a + value in quotes to keep its whitespace. `.claude/hooks/compose_escaping_check.py` + checks this before a push. + +The **only** strings that stay in Android `res/` are the ones a platform API +reads synchronously under a deadline, where a `suspend` read cannot run: a +foreground service's `startForeground` notification, a notification channel +created during service startup, PiP `RemoteAction`s, napplet capability labels +read in the sandbox process, and `AndroidManifest.xml` / `res/xml` references. +Most of those are *copies* of a key that also lives in `commonsUI`. See +`commons/plans/2026-09-22-strings-to-compose-resources.md` ("the +synchronous-platform tier"). If you think a new string belongs in that tier, +say why in the PR. + +To move keys that already landed in Android `res/`, use +`tools/strings-migrate/migrate.py <keys…>`. It moves every locale byte-for-byte, +so Crowdin sees a pure move. Then repoint `R.string.x` → `Res.string.x`. + ## Icons The Material Symbols font bundled at diff --git a/.claude/skills/android-expert/SKILL.md b/.claude/skills/android-expert/SKILL.md index e2483221d0..19b79ec6a1 100644 --- a/.claude/skills/android-expert/SKILL.md +++ b/.claude/skills/android-expert/SKILL.md @@ -875,18 +875,33 @@ fun ProfileScreen( ### 3. Resource Access +**Strings do not go in `amethyst/src/main/res`.** New user-visible strings live in +`commonsUI/src/commonMain/composeResources/values/strings.xml` and are read as +`Res.string.x`, even on an Android-only screen. See "Strings" in `.claude/CLAUDE.md` +for the rule and the small platform tier that is the only exception. + ```kotlin +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.button_clicked +import com.vitorpamplona.amethyst.commons.resources.button_label +import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.ui.stringRes + @Composable fun LocalizedButton() { val context = LocalContext.current + val scope = rememberCoroutineScope() Button( onClick = { - val message = context.getString(R.string.button_clicked) - Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + // No blocking accessor outside composition: read it from a coroutine, + // or resolve it in composition and capture the String. + scope.launch { + Toast.makeText(context, loadStringRes(Res.string.button_clicked), Toast.LENGTH_SHORT).show() + } } ) { - Text(stringResource(R.string.button_label)) + Text(stringRes(Res.string.button_label)) } } ``` diff --git a/.claude/skills/find-missing-translations/SKILL.md b/.claude/skills/find-missing-translations/SKILL.md index b41533cf3e..7547c5c46d 100644 --- a/.claude/skills/find-missing-translations/SKILL.md +++ b/.claude/skills/find-missing-translations/SKILL.md @@ -26,6 +26,13 @@ There are two separate `strings.xml` trees, each with its own default `values/` | **amethyst** (Android app) | `amethyst/src/main/res/values/strings.xml` | `amethyst/src/main/res/values-<locale>/strings.xml` | | **commonsUI** (KMP Compose resources, shared by Android + Desktop) | `commonsUI/src/commonMain/composeResources/values/strings.xml` | `commonsUI/src/commonMain/composeResources/values-<locale>/strings.xml` | +**New keys go in the `commonsUI` tree, always.** The amethyst tree is frozen at +the small synchronous-platform tier (foreground-service and notification-channel +text, PiP actions, napplet capability labels, manifest references). If you find +a new feature key in it, it belongs in `commonsUI`. Move it with +`tools/strings-migrate/migrate.py` rather than translating it where it is. See +"Strings" in `.claude/CLAUDE.md`. + The `commonsUI` tree appeared when shared event-renderer composables were extracted out of `amethyst/` into `commons/` — now `commonsUI/` since the UI split (Compose Multiplatform `stringResource`). It is **not** a copy of the amethyst tree — the vast majority of its keys are commons-only; only a small handful overlap. Every diff/count/translate command below works on either tree by swapping the base path — **run the whole technique once per tree** and report them separately (each maps to its own Crowdin file, so the counts should reconcile against two different Crowdin UI numbers). **Locale-qualifier caveat:** `commons` uses the same region-qualified locale dirs as amethyst for our four targets (`values-cs`, `values-de-rDE`, `values-sv-rSE`, `values-pt-rBR`), but the *full* set of locale dirs differs between trees. Enumerate `values-*` under each tree's own base rather than assuming they match. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt index bb977a4184..769c911ae4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt @@ -43,17 +43,57 @@ 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.res.pluralStringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.backups.BackupEventType import com.vitorpamplona.amethyst.commons.model.backups.ReplaceableBackupConflict import com.vitorpamplona.amethyst.commons.model.navigation.Route +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.backup_card_accept +import com.vitorpamplona.amethyst.commons.resources.backup_card_and_added +import com.vitorpamplona.amethyst.commons.resources.backup_card_and_blocked +import com.vitorpamplona.amethyst.commons.resources.backup_card_and_followed +import com.vitorpamplona.amethyst.commons.resources.backup_card_and_joined +import com.vitorpamplona.amethyst.commons.resources.backup_card_and_muted +import com.vitorpamplona.amethyst.commons.resources.backup_card_app_settings +import com.vitorpamplona.amethyst.commons.resources.backup_card_concord_emptied +import com.vitorpamplona.amethyst.commons.resources.backup_card_concord_rewritten +import com.vitorpamplona.amethyst.commons.resources.backup_card_dm_relays_lost +import com.vitorpamplona.amethyst.commons.resources.backup_card_feeds_removed +import com.vitorpamplona.amethyst.commons.resources.backup_card_follows_dropped +import com.vitorpamplona.amethyst.commons.resources.backup_card_hashtags_unfollowed +import com.vitorpamplona.amethyst.commons.resources.backup_card_indexer_relays_lost +import com.vitorpamplona.amethyst.commons.resources.backup_card_key_package_relays_lost +import com.vitorpamplona.amethyst.commons.resources.backup_card_leave_chats +import com.vitorpamplona.amethyst.commons.resources.backup_card_leave_communities +import com.vitorpamplona.amethyst.commons.resources.backup_card_leave_groups +import com.vitorpamplona.amethyst.commons.resources.backup_card_leave_rooms +import com.vitorpamplona.amethyst.commons.resources.backup_card_mutes_visible +import com.vitorpamplona.amethyst.commons.resources.backup_card_nip65_lost +import com.vitorpamplona.amethyst.commons.resources.backup_card_nutzap_key_removed +import com.vitorpamplona.amethyst.commons.resources.backup_card_nutzap_key_replaced +import com.vitorpamplona.amethyst.commons.resources.backup_card_nutzap_mints_removed +import com.vitorpamplona.amethyst.commons.resources.backup_card_offers_removed +import com.vitorpamplona.amethyst.commons.resources.backup_card_payment_targets_removed +import com.vitorpamplona.amethyst.commons.resources.backup_card_places_unfollowed +import com.vitorpamplona.amethyst.commons.resources.backup_card_private_outbox_lost +import com.vitorpamplona.amethyst.commons.resources.backup_card_profile_lost_fields +import com.vitorpamplona.amethyst.commons.resources.backup_card_relay_feeds_lost +import com.vitorpamplona.amethyst.commons.resources.backup_card_search_relays_lost +import com.vitorpamplona.amethyst.commons.resources.backup_card_trust_services_changed +import com.vitorpamplona.amethyst.commons.resources.backup_card_trusted_lost +import com.vitorpamplona.amethyst.commons.resources.backup_card_unblocked +import com.vitorpamplona.amethyst.commons.resources.backup_card_wallet_emptied +import com.vitorpamplona.amethyst.commons.resources.backup_card_wallet_rewritten +import com.vitorpamplona.amethyst.commons.resources.backup_conflict_count_added +import com.vitorpamplona.amethyst.commons.resources.backup_conflict_count_changed +import com.vitorpamplona.amethyst.commons.resources.backup_conflict_count_removed +import com.vitorpamplona.amethyst.commons.resources.backup_conflict_title +import com.vitorpamplona.amethyst.commons.resources.backup_review_changed_by_other_app import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot @@ -79,6 +119,9 @@ import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListDiff import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataDiff import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListDiff import com.vitorpamplona.quartz.nipB1Bolt12Zaps.offer.Bolt12OfferListDiff +import org.jetbrains.compose.resources.PluralStringResource +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.pluralStringResource /** * Cards at the top of Home's feed, one per open [ReplaceableBackupConflict]: the user @@ -125,34 +168,34 @@ private fun headlineOf( when (diff) { // Counted, not "shrank": a version that drops 3 follows and adds 100 still raises // a conflict, and calling that a shrink contradicts the card's own bar. - is ContactListDiff -> plural(R.plurals.backup_card_follows_dropped, diff.follows.removed.size) - is MuteListDiff -> plural(R.plurals.backup_card_mutes_visible, diff.publicMutes.removed.size) - is MetadataDiff -> plural(R.plurals.backup_card_profile_lost_fields, counts.removed) - is AdvertisedRelayListDiff -> plural(R.plurals.backup_card_nip65_lost, diff.relays.removed.size) + is ContactListDiff -> plural(Res.plurals.backup_card_follows_dropped, diff.follows.removed.size) + is MuteListDiff -> plural(Res.plurals.backup_card_mutes_visible, diff.publicMutes.removed.size) + is MetadataDiff -> plural(Res.plurals.backup_card_profile_lost_fields, counts.removed) + is AdvertisedRelayListDiff -> plural(Res.plurals.backup_card_nip65_lost, diff.relays.removed.size) is RelayListDiff -> relayListHeadline(conflict.eventType, diff.relays.removed.size) - is ChannelListDiff -> plural(R.plurals.backup_card_leave_chats, diff.channels.removed.size) - is CommunityListDiff -> plural(R.plurals.backup_card_leave_communities, diff.communities.removed.size) - is EphemeralChatListDiff -> plural(R.plurals.backup_card_leave_rooms, diff.rooms.removed.size) - is SimpleGroupListDiff -> plural(R.plurals.backup_card_leave_groups, diff.groups.removed.size) - is FavoriteAlgoFeedsListDiff -> plural(R.plurals.backup_card_feeds_removed, diff.feeds.removed.size) - is HashtagListDiff -> plural(R.plurals.backup_card_hashtags_unfollowed, diff.hashtags.removed.size) - is GeohashListDiff -> plural(R.plurals.backup_card_places_unfollowed, diff.geohashes.removed.size) - is TrustProviderListDiff -> plural(R.plurals.backup_card_trust_services_changed, diff.providers.removed.size + diff.providers.changed.size) + is ChannelListDiff -> plural(Res.plurals.backup_card_leave_chats, diff.channels.removed.size) + is CommunityListDiff -> plural(Res.plurals.backup_card_leave_communities, diff.communities.removed.size) + is EphemeralChatListDiff -> plural(Res.plurals.backup_card_leave_rooms, diff.rooms.removed.size) + is SimpleGroupListDiff -> plural(Res.plurals.backup_card_leave_groups, diff.groups.removed.size) + is FavoriteAlgoFeedsListDiff -> plural(Res.plurals.backup_card_feeds_removed, diff.feeds.removed.size) + is HashtagListDiff -> plural(Res.plurals.backup_card_hashtags_unfollowed, diff.hashtags.removed.size) + is GeohashListDiff -> plural(Res.plurals.backup_card_places_unfollowed, diff.geohashes.removed.size) + is TrustProviderListDiff -> plural(Res.plurals.backup_card_trust_services_changed, diff.providers.removed.size + diff.providers.changed.size) is NutzapInfoDiff -> when { - diff.p2pkPubkey?.after == null && diff.p2pkPubkey != null -> stringRes(R.string.backup_card_nutzap_key_removed) - diff.p2pkPubkey != null -> stringRes(R.string.backup_card_nutzap_key_replaced) - else -> plural(R.plurals.backup_card_nutzap_mints_removed, diff.mints.removed.size) + diff.p2pkPubkey?.after == null && diff.p2pkPubkey != null -> stringRes(Res.string.backup_card_nutzap_key_removed) + diff.p2pkPubkey != null -> stringRes(Res.string.backup_card_nutzap_key_replaced) + else -> plural(Res.plurals.backup_card_nutzap_mints_removed, diff.mints.removed.size) } - is PaymentTargetsDiff -> plural(R.plurals.backup_card_payment_targets_removed, diff.targets.removed.size) - is Bolt12OfferListDiff -> plural(R.plurals.backup_card_offers_removed, diff.offers.removed.size) - is CashuWalletDiff -> encryptedHeadline(R.string.backup_card_wallet_emptied, R.string.backup_card_wallet_rewritten, diff.wallet) - is ConcordCommunityListDiff -> encryptedHeadline(R.string.backup_card_concord_emptied, R.string.backup_card_concord_rewritten, diff.communities) - is AppSpecificDataDiff -> stringRes(R.string.backup_card_app_settings) + is PaymentTargetsDiff -> plural(Res.plurals.backup_card_payment_targets_removed, diff.targets.removed.size) + is Bolt12OfferListDiff -> plural(Res.plurals.backup_card_offers_removed, diff.offers.removed.size) + is CashuWalletDiff -> encryptedHeadline(Res.string.backup_card_wallet_emptied, Res.string.backup_card_wallet_rewritten, diff.wallet) + is ConcordCommunityListDiff -> encryptedHeadline(Res.string.backup_card_concord_emptied, Res.string.backup_card_concord_rewritten, diff.communities) + is AppSpecificDataDiff -> stringRes(Res.string.backup_card_app_settings) else -> null } // A headline counting removed items reads wrong when only encrypted items were lost. - if (specific.isNullOrEmpty()) return stringRes(R.string.backup_conflict_title, stringRes(eventTypeName(conflict.eventType))) + if (specific.isNullOrEmpty()) return stringRes(Res.string.backup_conflict_title, stringRes(eventTypeName(conflict.eventType))) // Headlines lead with the loss; what the other app added to the same list is said too, // so a list that dropped 3 and gained 40 doesn't read as if it only shrank. The wording // follows the headline's: following people, muting, joining chats, blocking relays. @@ -160,11 +203,11 @@ private fun headlineOf( if (added <= 0) return specific val tail = when { - diff is ContactListDiff -> R.plurals.backup_card_and_followed - diff is MuteListDiff -> R.plurals.backup_card_and_muted - diff is ChannelListDiff || diff is CommunityListDiff || diff is EphemeralChatListDiff || diff is SimpleGroupListDiff -> R.plurals.backup_card_and_joined - conflict.eventType == BackupEventType.BLOCKED_RELAYS -> R.plurals.backup_card_and_blocked - else -> R.plurals.backup_card_and_added + diff is ContactListDiff -> Res.plurals.backup_card_and_followed + diff is MuteListDiff -> Res.plurals.backup_card_and_muted + diff is ChannelListDiff || diff is CommunityListDiff || diff is EphemeralChatListDiff || diff is SimpleGroupListDiff -> Res.plurals.backup_card_and_joined + conflict.eventType == BackupEventType.BLOCKED_RELAYS -> Res.plurals.backup_card_and_blocked + else -> Res.plurals.backup_card_and_added } return pluralStringResource(tail, added, specific, added) } @@ -208,7 +251,7 @@ private fun relayListHasHeadline(type: BackupEventType): Boolean = /** The count's plural, or "" when nothing of that kind was removed. */ @Composable private fun plural( - id: Int, + id: PluralStringResource, count: Int, ): String = if (count > 0) pluralStringResource(id, count, count) else "" @@ -218,21 +261,21 @@ private fun relayListHeadline( removed: Int, ): String = when (type) { - BackupEventType.DM_RELAYS -> plural(R.plurals.backup_card_dm_relays_lost, removed) - BackupEventType.KEY_PACKAGE_RELAYS -> plural(R.plurals.backup_card_key_package_relays_lost, removed) - BackupEventType.SEARCH_RELAYS -> plural(R.plurals.backup_card_search_relays_lost, removed) - BackupEventType.INDEXER_RELAYS -> plural(R.plurals.backup_card_indexer_relays_lost, removed) - BackupEventType.RELAY_FEEDS -> plural(R.plurals.backup_card_relay_feeds_lost, removed) - BackupEventType.PRIVATE_OUTBOX_RELAYS -> plural(R.plurals.backup_card_private_outbox_lost, removed) - BackupEventType.TRUSTED_RELAYS -> plural(R.plurals.backup_card_trusted_lost, removed) - BackupEventType.BLOCKED_RELAYS -> plural(R.plurals.backup_card_unblocked, removed) + BackupEventType.DM_RELAYS -> plural(Res.plurals.backup_card_dm_relays_lost, removed) + BackupEventType.KEY_PACKAGE_RELAYS -> plural(Res.plurals.backup_card_key_package_relays_lost, removed) + BackupEventType.SEARCH_RELAYS -> plural(Res.plurals.backup_card_search_relays_lost, removed) + BackupEventType.INDEXER_RELAYS -> plural(Res.plurals.backup_card_indexer_relays_lost, removed) + BackupEventType.RELAY_FEEDS -> plural(Res.plurals.backup_card_relay_feeds_lost, removed) + BackupEventType.PRIVATE_OUTBOX_RELAYS -> plural(Res.plurals.backup_card_private_outbox_lost, removed) + BackupEventType.TRUSTED_RELAYS -> plural(Res.plurals.backup_card_trusted_lost, removed) + BackupEventType.BLOCKED_RELAYS -> plural(Res.plurals.backup_card_unblocked, removed) else -> "" } @Composable private fun encryptedHeadline( - emptied: Int, - rewritten: Int, + emptied: StringResource, + rewritten: StringResource, change: ContentChange, ): String = if (change == ContentChange.CLEARED) stringRes(emptied) else stringRes(rewritten) @@ -262,7 +305,7 @@ private fun LeadConflictCard( Column(Modifier.weight(1f)) { Text(headlineOf(conflict, counts), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold, maxLines = 3, overflow = TextOverflow.Ellipsis) Text( - stringRes(R.string.backup_review_changed_by_other_app, timeAgoNoDot(conflict.cause.createdAt)), + stringRes(Res.string.backup_review_changed_by_other_app, timeAgoNoDot(conflict.cause.createdAt)), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.placeholderText, ) @@ -296,9 +339,9 @@ private fun LeadConflictCard( private fun CountChips(counts: ConflictCounts) { val tones = conflictTones() Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - if (counts.removed > 0) StatusTag(stringRes(R.string.backup_conflict_count_removed, counts.removed.toString()), tones.removed) - if (counts.added > 0) StatusTag(stringRes(R.string.backup_conflict_count_added, counts.added.toString()), tones.added) - if (counts.changed > 0) StatusTag(stringRes(R.string.backup_conflict_count_changed, counts.changed.toString()), tones.changed) + if (counts.removed > 0) StatusTag(stringRes(Res.string.backup_conflict_count_removed, counts.removed.toString()), tones.removed) + if (counts.added > 0) StatusTag(stringRes(Res.string.backup_conflict_count_added, counts.added.toString()), tones.added) + if (counts.changed > 0) StatusTag(stringRes(Res.string.backup_conflict_count_changed, counts.changed.toString()), tones.changed) } } @@ -342,6 +385,6 @@ private fun AcceptButton(onAccept: () -> Unit) { modifier = Modifier.height(36.dp), contentPadding = PaddingValues(horizontal = 14.dp), ) { - Text(stringRes(R.string.backup_card_accept), fontWeight = FontWeight.SemiBold) + Text(stringRes(Res.string.backup_card_accept), fontWeight = FontWeight.SemiBold) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictEventViews.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictEventViews.kt index 419bd3b2a2..1916f09286 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictEventViews.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictEventViews.kt @@ -62,7 +62,6 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.backups.ReplaceableBackupConflict @@ -70,6 +69,67 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.navigation.routeFor import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.backup_entry_hashtag +import com.vitorpamplona.amethyst.commons.resources.backup_entry_mint +import com.vitorpamplona.amethyst.commons.resources.backup_entry_nutzap_key +import com.vitorpamplona.amethyst.commons.resources.backup_entry_person +import com.vitorpamplona.amethyst.commons.resources.backup_entry_profile_field +import com.vitorpamplona.amethyst.commons.resources.backup_entry_thread +import com.vitorpamplona.amethyst.commons.resources.backup_entry_word +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_about +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_banner +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_birthday +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_bot +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_clink_offer +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_display_name +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_lud06 +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_lud16 +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_name +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_nip05 +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_picture +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_pronouns +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_website +import com.vitorpamplona.amethyst.commons.resources.backup_review_demoted_tag +import com.vitorpamplona.amethyst.commons.resources.backup_review_dropped_count +import com.vitorpamplona.amethyst.commons.resources.backup_review_dropped_tag +import com.vitorpamplona.amethyst.commons.resources.backup_review_gone +import com.vitorpamplona.amethyst.commons.resources.backup_review_inbox +import com.vitorpamplona.amethyst.commons.resources.backup_review_inbox_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_review_joined +import com.vitorpamplona.amethyst.commons.resources.backup_review_kept_count +import com.vitorpamplona.amethyst.commons.resources.backup_review_key_removed +import com.vitorpamplona.amethyst.commons.resources.backup_review_key_replaced +import com.vitorpamplona.amethyst.commons.resources.backup_review_key_warning +import com.vitorpamplona.amethyst.commons.resources.backup_review_left +import com.vitorpamplona.amethyst.commons.resources.backup_review_left_tag +import com.vitorpamplona.amethyst.commons.resources.backup_review_names_and_more +import com.vitorpamplona.amethyst.commons.resources.backup_review_new +import com.vitorpamplona.amethyst.commons.resources.backup_review_new_count +import com.vitorpamplona.amethyst.commons.resources.backup_review_new_tag +import com.vitorpamplona.amethyst.commons.resources.backup_review_newly_muted +import com.vitorpamplona.amethyst.commons.resources.backup_review_not_set +import com.vitorpamplona.amethyst.commons.resources.backup_review_nothing_here +import com.vitorpamplona.amethyst.commons.resources.backup_review_outbox +import com.vitorpamplona.amethyst.commons.resources.backup_review_outbox_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_review_private_added_body +import com.vitorpamplona.amethyst.commons.resources.backup_review_private_added_title +import com.vitorpamplona.amethyst.commons.resources.backup_review_private_changed_body +import com.vitorpamplona.amethyst.commons.resources.backup_review_private_changed_title +import com.vitorpamplona.amethyst.commons.resources.backup_review_private_wiped_body +import com.vitorpamplona.amethyst.commons.resources.backup_review_private_wiped_title +import com.vitorpamplona.amethyst.commons.resources.backup_review_renamed +import com.vitorpamplona.amethyst.commons.resources.backup_review_renamed_tag +import com.vitorpamplona.amethyst.commons.resources.backup_review_saved +import com.vitorpamplona.amethyst.commons.resources.backup_review_search_people +import com.vitorpamplona.amethyst.commons.resources.backup_review_tab_dropped +import com.vitorpamplona.amethyst.commons.resources.backup_review_tab_edited +import com.vitorpamplona.amethyst.commons.resources.backup_review_tab_new +import com.vitorpamplona.amethyst.commons.resources.backup_review_units_tag +import com.vitorpamplona.amethyst.commons.resources.backup_review_unmuted +import com.vitorpamplona.amethyst.commons.resources.backup_review_unmuted_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_review_was_named +import com.vitorpamplona.amethyst.commons.resources.backup_review_words_and_hashtags import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings @@ -111,6 +171,7 @@ import com.vitorpamplona.quartz.nip61Nutzaps.info.tags.NutzapMintTag import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListDiff import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayType +import org.jetbrains.compose.resources.StringResource private const val GRID_COLUMNS = 4 private const val PEOPLE_PREVIEW = 6 @@ -168,7 +229,7 @@ private fun LazyListScope.followListItems( item(key = "follow-hero", contentType = "hero") { val tones = conflictTones() Column(Pad.padding(top = 8.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { - HeroCounts(stringRes(R.string.backup_review_saved), saved, stringRes(R.string.backup_review_new), new) + HeroCounts(stringRes(Res.string.backup_review_saved), saved, stringRes(Res.string.backup_review_new), new) SplitBar( listOf( BarSegment(saved - dropped, tones.kept), @@ -177,9 +238,9 @@ private fun LazyListScope.followListItems( ), ) Row(horizontalArrangement = Arrangement.spacedBy(14.dp)) { - LegendItem(tones.kept, stringRes(R.string.backup_review_kept_count, (saved - dropped).toString())) - LegendItem(tones.removed, stringRes(R.string.backup_review_dropped_count, dropped.toString())) - LegendItem(tones.added, stringRes(R.string.backup_review_new_count, gained.toString())) + LegendItem(tones.kept, stringRes(Res.string.backup_review_kept_count, (saved - dropped).toString())) + LegendItem(tones.removed, stringRes(Res.string.backup_review_dropped_count, dropped.toString())) + LegendItem(tones.added, stringRes(Res.string.backup_review_new_count, gained.toString())) } } } @@ -188,9 +249,9 @@ private fun LazyListScope.followListItems( ReviewTabs( labels = listOf( - stringRes(R.string.backup_review_tab_dropped, dropped.toString()), - stringRes(R.string.backup_review_tab_new, gained.toString()), - stringRes(R.string.backup_review_tab_edited, edited.toString()), + stringRes(Res.string.backup_review_tab_dropped, dropped.toString()), + stringRes(Res.string.backup_review_tab_new, gained.toString()), + stringRes(Res.string.backup_review_tab_edited, edited.toString()), ), selected = ui.tab, onSelect = { @@ -223,7 +284,7 @@ private fun LazyListScope.followListItems( if (filtered.isEmpty()) { item(key = "follow-empty", contentType = "note") { - DetailText(stringRes(R.string.backup_review_nothing_here), Pad.padding(vertical = 24.dp)) + DetailText(stringRes(Res.string.backup_review_nothing_here), Pad.padding(vertical = 24.dp)) } } } @@ -359,7 +420,7 @@ private fun PeopleSearch( singleLine = true, shape = RoundedCornerShape(14.dp), leadingIcon = { Icon(symbol = MaterialSymbols.Search, contentDescription = null) }, - placeholder = { Text(stringRes(R.string.backup_review_search_people, count.toString())) }, + placeholder = { Text(stringRes(Res.string.backup_review_search_people, count.toString())) }, ) } @@ -388,15 +449,15 @@ private fun LazyListScope.muteListItems( Icon(symbol = MaterialSymbols.Shield, contentDescription = null, tint = tones.removed, modifier = Modifier.size(44.dp)) Column { Text( - stringRes(R.string.backup_review_unmuted, removed.size.toString()), + stringRes(Res.string.backup_review_unmuted, removed.size.toString()), fontSize = 30.sp, lineHeight = 32.sp, fontWeight = FontWeight.Bold, color = tones.removed, ) - Text(stringRes(R.string.backup_review_unmuted_explainer), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.placeholderText) + Text(stringRes(Res.string.backup_review_unmuted_explainer), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.placeholderText) if (added.isNotEmpty()) { - Text(stringRes(R.string.backup_review_newly_muted, added.size.toString()), style = MaterialTheme.typography.bodySmall, color = tones.added) + Text(stringRes(Res.string.backup_review_newly_muted, added.size.toString()), style = MaterialTheme.typography.bodySmall, color = tones.added) } } } @@ -406,17 +467,17 @@ private fun LazyListScope.muteListItems( item(key = "mute-tiles", contentType = "tiles") { val tones = conflictTones() Row(Pad.padding(top = 12.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) { - StatTile("−${people.size}", stringRes(R.string.backup_entry_person), tones.removed, Modifier.weight(1f)) - StatTile("−${words.size}", stringRes(R.string.backup_entry_word), tones.removed, Modifier.weight(1f)) - StatTile("−${hashtags.size}", stringRes(R.string.backup_entry_hashtag), tones.removed, Modifier.weight(1f)) - StatTile("−${threads.size}", stringRes(R.string.backup_entry_thread), tones.removed, Modifier.weight(1f)) + StatTile("−${people.size}", stringRes(Res.string.backup_entry_person), tones.removed, Modifier.weight(1f)) + StatTile("−${words.size}", stringRes(Res.string.backup_entry_word), tones.removed, Modifier.weight(1f)) + StatTile("−${hashtags.size}", stringRes(Res.string.backup_entry_hashtag), tones.removed, Modifier.weight(1f)) + StatTile("−${threads.size}", stringRes(Res.string.backup_entry_thread), tones.removed, Modifier.weight(1f)) } } if (people.isNotEmpty()) { item(key = "mute-people", contentType = "facepile") { Column(Pad.padding(top = 18.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - SectionCaption(stringRes(R.string.backup_entry_person)) + SectionCaption(stringRes(Res.string.backup_entry_person)) FacePile(people, accountViewModel) { ui.showAllPeople = !ui.showAllPeople } } } @@ -427,7 +488,7 @@ private fun LazyListScope.muteListItems( item(key = "mute-words", contentType = "pills") { val tones = conflictTones() Column(Pad.padding(top = 18.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - SectionCaption(stringRes(R.string.backup_review_words_and_hashtags)) + SectionCaption(stringRes(Res.string.backup_review_words_and_hashtags)) FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { (words.map { "\"$it\"" } + hashtags).forEach { StruckPill(it, tones.removed) } } @@ -436,7 +497,7 @@ private fun LazyListScope.muteListItems( } if (threads.isNotEmpty()) { - item(key = "mute-threads-caption", contentType = "caption") { SectionCaption(stringRes(R.string.backup_entry_thread), Pad.padding(top = 18.dp)) } + item(key = "mute-threads-caption", contentType = "caption") { SectionCaption(stringRes(Res.string.backup_entry_thread), Pad.padding(top = 18.dp)) } items(threads, key = { "mute-thread-$it" }, contentType = { "note" }) { id -> LoadNote(id) { note -> if (note != null) { @@ -463,7 +524,7 @@ private fun FacePile( val names = people.take(2).map { observedName(it, accountViewModel) } val summary = if (people.size > 2) { - stringRes(R.string.backup_review_names_and_more, names.joinToString(", "), (people.size - 2).toString()) + stringRes(Res.string.backup_review_names_and_more, names.joinToString(", "), (people.size - 2).toString()) } else { names.joinToString(", ") } @@ -564,9 +625,9 @@ internal fun PrivateItemsCard( val tones = conflictTones() val (title, body) = when (change) { - ContentChange.CLEARED -> R.string.backup_review_private_wiped_title to R.string.backup_review_private_wiped_body - ContentChange.ADDED -> R.string.backup_review_private_added_title to R.string.backup_review_private_added_body - else -> R.string.backup_review_private_changed_title to R.string.backup_review_private_changed_body + ContentChange.CLEARED -> Res.string.backup_review_private_wiped_title to Res.string.backup_review_private_wiped_body + ContentChange.ADDED -> Res.string.backup_review_private_added_title to Res.string.backup_review_private_added_body + else -> Res.string.backup_review_private_changed_title to Res.string.backup_review_private_changed_body } Row( modifier @@ -592,28 +653,28 @@ internal fun PrivateItemsCard( // --------------------------------------------------------------------------------------- private class ProfileField( - val labelRes: Int, + val labelRes: StringResource, val change: ValueChange<String>, val isImage: Boolean = false, ) private fun profileFields(diff: MetadataDiff): List<ProfileField> = listOfNotNull( - diff.picture?.let { ProfileField(R.string.backup_profile_field_picture, it, isImage = true) }, - diff.banner?.let { ProfileField(R.string.backup_profile_field_banner, it, isImage = true) }, - diff.name?.let { ProfileField(R.string.backup_profile_field_name, it) }, - diff.displayName?.let { ProfileField(R.string.backup_profile_field_display_name, it) }, - diff.about?.let { ProfileField(R.string.backup_profile_field_about, it) }, - diff.nip05?.let { ProfileField(R.string.backup_profile_field_nip05, it) }, - diff.website?.let { ProfileField(R.string.backup_profile_field_website, it) }, - diff.lud16?.let { ProfileField(R.string.backup_profile_field_lud16, it) }, - diff.lud06?.let { ProfileField(R.string.backup_profile_field_lud06, it) }, - diff.clinkOffer?.let { ProfileField(R.string.backup_profile_field_clink_offer, it) }, - diff.pronouns?.let { ProfileField(R.string.backup_profile_field_pronouns, it) }, - diff.bot?.let { ProfileField(R.string.backup_profile_field_bot, ValueChange(it.before?.toString(), it.after?.toString())) }, + diff.picture?.let { ProfileField(Res.string.backup_profile_field_picture, it, isImage = true) }, + diff.banner?.let { ProfileField(Res.string.backup_profile_field_banner, it, isImage = true) }, + diff.name?.let { ProfileField(Res.string.backup_profile_field_name, it) }, + diff.displayName?.let { ProfileField(Res.string.backup_profile_field_display_name, it) }, + diff.about?.let { ProfileField(Res.string.backup_profile_field_about, it) }, + diff.nip05?.let { ProfileField(Res.string.backup_profile_field_nip05, it) }, + diff.website?.let { ProfileField(Res.string.backup_profile_field_website, it) }, + diff.lud16?.let { ProfileField(Res.string.backup_profile_field_lud16, it) }, + diff.lud06?.let { ProfileField(Res.string.backup_profile_field_lud06, it) }, + diff.clinkOffer?.let { ProfileField(Res.string.backup_profile_field_clink_offer, it) }, + diff.pronouns?.let { ProfileField(Res.string.backup_profile_field_pronouns, it) }, + diff.bot?.let { ProfileField(Res.string.backup_profile_field_bot, ValueChange(it.before?.toString(), it.after?.toString())) }, diff.birthday?.let { ProfileField( - R.string.backup_profile_field_birthday, + Res.string.backup_profile_field_birthday, ValueChange( it.before?.let { b -> listOfNotNull(b.year, b.month, b.day).joinToString("-") }, it.after?.let { a -> listOfNotNull(a.year, a.month, a.day).joinToString("-") }, @@ -636,8 +697,8 @@ private fun LazyListScope.profileItems( item(key = "profile-cards", contentType = "profile-cards") { val tones = conflictTones() Row(Pad.padding(top = 8.dp), horizontalArrangement = Arrangement.spacedBy(10.dp)) { - MiniProfileCard(saved, pubKey, stringRes(R.string.backup_review_saved), MaterialTheme.colorScheme.outlineVariant, accountViewModel, Modifier.weight(1f)) - MiniProfileCard(incoming, pubKey, stringRes(R.string.backup_review_new), tones.removed.copy(alpha = 0.5f), accountViewModel, Modifier.weight(1f)) + MiniProfileCard(saved, pubKey, stringRes(Res.string.backup_review_saved), MaterialTheme.colorScheme.outlineVariant, accountViewModel, Modifier.weight(1f)) + MiniProfileCard(incoming, pubKey, stringRes(Res.string.backup_review_new), tones.removed.copy(alpha = 0.5f), accountViewModel, Modifier.weight(1f)) } } @@ -656,7 +717,7 @@ private fun LazyListScope.profileItems( } } // Linked identities and fields this app doesn't model use the generic sections. - genericDiffItems(buildRows(presentation, skipGroups = setOf(R.string.backup_entry_profile_field)), accountViewModel, nav) + genericDiffItems(buildRows(presentation, skipGroups = setOf(Res.string.backup_entry_profile_field)), accountViewModel, nav) } @Composable @@ -735,10 +796,10 @@ private fun FlowRowDiff( FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(6.dp), itemVerticalAlignment = Alignment.CenterVertically) { if (before != null) DiffChip(clip(before), tones.removed, struck = true) when { - after == null -> StatusTag(stringRes(R.string.backup_review_gone), tones.removed) + after == null -> StatusTag(stringRes(Res.string.backup_review_gone), tones.removed) before == null -> { DiffChip(clip(after), tones.added, struck = false) - StatusTag(stringRes(R.string.backup_review_new_tag), tones.added) + StatusTag(stringRes(Res.string.backup_review_new_tag), tones.added) } else -> { Text("→", color = MaterialTheme.colorScheme.placeholderText) @@ -779,7 +840,7 @@ private fun ImageThumb( contentAlignment = Alignment.Center, ) { if (url == null) { - Text(stringRes(R.string.backup_review_not_set), style = MaterialTheme.typography.labelSmall, color = color) + Text(stringRes(Res.string.backup_review_not_set), style = MaterialTheme.typography.labelSmall, color = color) } else { MyAsyncImage( imageUrl = url, @@ -843,14 +904,14 @@ private fun LazyListScope.nip65Items( val outbox = lane(saved, new, writes) val inbox = lane(saved, new, reads) - laneItems("outbox", stringRes = R.string.backup_review_outbox, subRes = R.string.backup_review_outbox_explainer, rows = outbox, nav = nav) - laneItems("inbox", stringRes = R.string.backup_review_inbox, subRes = R.string.backup_review_inbox_explainer, rows = inbox, nav = nav) + laneItems("outbox", stringRes = Res.string.backup_review_outbox, subRes = Res.string.backup_review_outbox_explainer, rows = outbox, nav = nav) + laneItems("inbox", stringRes = Res.string.backup_review_inbox, subRes = Res.string.backup_review_inbox_explainer, rows = inbox, nav = nav) } private fun LazyListScope.laneItems( key: String, - stringRes: Int, - subRes: Int, + stringRes: StringResource, + subRes: StringResource, rows: List<LaneRow>, nav: INav, ) { @@ -873,9 +934,9 @@ private fun LaneRelayRow( val (color, tag) = when (row.state) { LaneState.KEPT -> tones.kept to null - LaneState.DROPPED -> tones.removed to R.string.backup_review_dropped_tag - LaneState.ADDED -> tones.added to R.string.backup_review_new_tag - LaneState.DEMOTED -> tones.changed to R.string.backup_review_demoted_tag + LaneState.DROPPED -> tones.removed to Res.string.backup_review_dropped_tag + LaneState.ADDED -> tones.added to Res.string.backup_review_new_tag + LaneState.DEMOTED -> tones.changed to Res.string.backup_review_demoted_tag } Row( Pad @@ -933,12 +994,12 @@ private fun LazyListScope.nutzapItems( ) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Icon(symbol = MaterialSymbols.Key, contentDescription = null, tint = tones.changed) - Text(stringRes(if (key.after == null) R.string.backup_review_key_removed else R.string.backup_review_key_replaced), style = MaterialTheme.typography.titleSmall) + Text(stringRes(if (key.after == null) Res.string.backup_review_key_removed else Res.string.backup_review_key_replaced), style = MaterialTheme.typography.titleSmall) } Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) { - KeyColumn(key.before, stringRes(R.string.backup_review_saved), tones.kept) + KeyColumn(key.before, stringRes(Res.string.backup_review_saved), tones.kept) Text("→", fontSize = 24.sp, color = MaterialTheme.colorScheme.placeholderText, modifier = Modifier.padding(horizontal = 18.dp)) - KeyColumn(key.after, stringRes(R.string.backup_review_new), tones.changed) + KeyColumn(key.after, stringRes(Res.string.backup_review_new), tones.changed) } if (key.after != null) { Row( @@ -950,7 +1011,7 @@ private fun LazyListScope.nutzapItems( horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Icon(symbol = MaterialSymbols.Warning, contentDescription = null, tint = tones.changed, modifier = Modifier.size(18.dp)) - Text(stringRes(R.string.backup_review_key_warning), style = MaterialTheme.typography.bodySmall, color = tones.changed) + Text(stringRes(Res.string.backup_review_key_warning), style = MaterialTheme.typography.bodySmall, color = tones.changed) } } } @@ -976,10 +1037,10 @@ private fun LazyListScope.nutzapItems( } + diff.mints.added.map { Triple(it.mintUrl, it.units.joinToString(), LaneState.ADDED) } if (mintRows.isNotEmpty()) { - item(key = "nutzap-mints", contentType = "caption") { SectionCaption(stringRes(R.string.backup_entry_mint), Pad.padding(top = 18.dp, bottom = 6.dp)) } + item(key = "nutzap-mints", contentType = "caption") { SectionCaption(stringRes(Res.string.backup_entry_mint), Pad.padding(top = 18.dp, bottom = 6.dp)) } items(mintRows, key = { "mint-" + it.first }, contentType = { "mint" }) { (url, units, state) -> MintRow(url, units, state) } } - genericDiffItems(buildRows(presentation, skipGroups = setOf(R.string.backup_entry_mint, R.string.backup_entry_nutzap_key)), accountViewModel, nav) + genericDiffItems(buildRows(presentation, skipGroups = setOf(Res.string.backup_entry_mint, Res.string.backup_entry_nutzap_key)), accountViewModel, nav) } @Composable @@ -993,7 +1054,7 @@ private fun KeyColumn( KeyFingerprint(hex, color) } else { Box(Modifier.size(96.dp).clip(RoundedCornerShape(18.dp)).border(2.dp, color, RoundedCornerShape(18.dp)), contentAlignment = Alignment.Center) { - Text(stringRes(R.string.backup_review_not_set), style = MaterialTheme.typography.labelSmall, color = color) + Text(stringRes(Res.string.backup_review_not_set), style = MaterialTheme.typography.labelSmall, color = color) } } StatusTag(label, color) @@ -1010,9 +1071,9 @@ private fun MintRow( val (color, tag) = when (state) { LaneState.KEPT -> MaterialTheme.colorScheme.onSurface to null - LaneState.DROPPED -> tones.removed to R.string.backup_review_dropped_tag - LaneState.ADDED -> tones.added to R.string.backup_review_new_tag - LaneState.DEMOTED -> tones.changed to R.string.backup_review_units_tag + LaneState.DROPPED -> tones.removed to Res.string.backup_review_dropped_tag + LaneState.ADDED -> tones.added to Res.string.backup_review_new_tag + LaneState.DEMOTED -> tones.changed to Res.string.backup_review_units_tag } Row( Pad @@ -1083,21 +1144,21 @@ private fun LazyListScope.groupItems( StatTile( diff.groups.removed.size .toString(), - stringRes(R.string.backup_review_left), + stringRes(Res.string.backup_review_left), tones.removed, Modifier.weight(1f), ) StatTile( diff.groups.added.size .toString(), - stringRes(R.string.backup_review_joined), + stringRes(Res.string.backup_review_joined), tones.added, Modifier.weight(1f), ) StatTile( diff.groups.changed.size .toString(), - stringRes(R.string.backup_review_renamed), + stringRes(Res.string.backup_review_renamed), tones.changed, Modifier.weight(1f), ) @@ -1165,7 +1226,7 @@ private fun GroupTileContent( Column { Text(name, style = MaterialTheme.typography.titleSmall, maxLines = 1, overflow = TextOverflow.Ellipsis) Text( - tile.oldName?.let { stringRes(R.string.backup_review_was_named, it) } ?: tile.group.relayUrl + tile.oldName?.let { stringRes(Res.string.backup_review_was_named, it) } ?: tile.group.relayUrl .removePrefix("wss://") .removeSuffix("/"), style = MaterialTheme.typography.labelSmall, @@ -1176,9 +1237,9 @@ private fun GroupTileContent( } } when (tile.state) { - LaneState.DROPPED -> StatusTag(stringRes(R.string.backup_review_left_tag), tones.removed, Modifier.align(Alignment.TopEnd)) - LaneState.ADDED -> StatusTag(stringRes(R.string.backup_review_new_tag), tones.added, Modifier.align(Alignment.TopEnd)) - LaneState.DEMOTED -> StatusTag(stringRes(R.string.backup_review_renamed_tag), tones.changed, Modifier.align(Alignment.TopEnd)) + LaneState.DROPPED -> StatusTag(stringRes(Res.string.backup_review_left_tag), tones.removed, Modifier.align(Alignment.TopEnd)) + LaneState.ADDED -> StatusTag(stringRes(Res.string.backup_review_new_tag), tones.added, Modifier.align(Alignment.TopEnd)) + LaneState.DEMOTED -> StatusTag(stringRes(Res.string.backup_review_renamed_tag), tones.changed, Modifier.align(Alignment.TopEnd)) LaneState.KEPT -> {} } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt index 55473cce27..b2e854c06b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt @@ -48,13 +48,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -63,6 +61,53 @@ import com.vitorpamplona.amethyst.commons.model.backups.ReplaceableBackupConflic import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.navigation.routeFor +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.backup_review_allowed_tag +import com.vitorpamplona.amethyst.commons.resources.backup_review_concord_body +import com.vitorpamplona.amethyst.commons.resources.backup_review_demoted_tag +import com.vitorpamplona.amethyst.commons.resources.backup_review_dropped_tag +import com.vitorpamplona.amethyst.commons.resources.backup_review_encrypted_added +import com.vitorpamplona.amethyst.commons.resources.backup_review_encrypted_changed +import com.vitorpamplona.amethyst.commons.resources.backup_review_encrypted_cleared +import com.vitorpamplona.amethyst.commons.resources.backup_review_feed +import com.vitorpamplona.amethyst.commons.resources.backup_review_hashtags_you_follow +import com.vitorpamplona.amethyst.commons.resources.backup_review_joined +import com.vitorpamplona.amethyst.commons.resources.backup_review_kept +import com.vitorpamplona.amethyst.commons.resources.backup_review_left +import com.vitorpamplona.amethyst.commons.resources.backup_review_left_tag +import com.vitorpamplona.amethyst.commons.resources.backup_review_listed_tag +import com.vitorpamplona.amethyst.commons.resources.backup_review_moved_tag +import com.vitorpamplona.amethyst.commons.resources.backup_review_new +import com.vitorpamplona.amethyst.commons.resources.backup_review_new_count_caption +import com.vitorpamplona.amethyst.commons.resources.backup_review_new_tag +import com.vitorpamplona.amethyst.commons.resources.backup_review_newly_blocked +import com.vitorpamplona.amethyst.commons.resources.backup_review_no_longer_blocked +import com.vitorpamplona.amethyst.commons.resources.backup_review_offer +import com.vitorpamplona.amethyst.commons.resources.backup_review_offers_gone +import com.vitorpamplona.amethyst.commons.resources.backup_review_payment_targets_gone +import com.vitorpamplona.amethyst.commons.resources.backup_review_payments_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_review_places_gone +import com.vitorpamplona.amethyst.commons.resources.backup_review_places_you_follow +import com.vitorpamplona.amethyst.commons.resources.backup_review_receiving_tag +import com.vitorpamplona.amethyst.commons.resources.backup_review_saved +import com.vitorpamplona.amethyst.commons.resources.backup_review_searching_tag +import com.vitorpamplona.amethyst.commons.resources.backup_review_service +import com.vitorpamplona.amethyst.commons.resources.backup_review_service_followers +import com.vitorpamplona.amethyst.commons.resources.backup_review_service_rank +import com.vitorpamplona.amethyst.commons.resources.backup_review_service_topics +import com.vitorpamplona.amethyst.commons.resources.backup_review_services_changed +import com.vitorpamplona.amethyst.commons.resources.backup_review_services_changed_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_review_still_blocked +import com.vitorpamplona.amethyst.commons.resources.backup_review_storing_tag +import com.vitorpamplona.amethyst.commons.resources.backup_review_topics_gone +import com.vitorpamplona.amethyst.commons.resources.backup_review_trust_warning +import com.vitorpamplona.amethyst.commons.resources.backup_review_trusted_tag +import com.vitorpamplona.amethyst.commons.resources.backup_review_unblocked +import com.vitorpamplona.amethyst.commons.resources.backup_review_unblocked_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_review_untrusted_tag +import com.vitorpamplona.amethyst.commons.resources.backup_review_wallet_body +import com.vitorpamplona.amethyst.commons.resources.backup_type_communities +import com.vitorpamplona.amethyst.commons.resources.backup_type_public_chats import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings @@ -118,6 +163,9 @@ import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEve import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.offer.Bolt12OfferListDiff import com.vitorpamplona.quartz.nipB1Bolt12Zaps.offer.Bolt12OfferListEvent +import org.jetbrains.compose.resources.PluralStringResource +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.pluralStringResource /** * Per-event layouts for the account's other lists: each relay list framed by what it is @@ -142,8 +190,8 @@ internal fun LazyListScope.listDiffItems( is TrustProviderListDiff -> trustProviderItems(diff, conflict, accountViewModel, nav) is PaymentTargetsDiff -> paymentTargetItems(diff, conflict) is Bolt12OfferListDiff -> offerItems(diff, conflict) - is CashuWalletDiff -> encryptedOnlyItems(MaterialSymbols.AccountBalanceWallet, diff.wallet, R.string.backup_review_wallet_body) - is ConcordCommunityListDiff -> encryptedOnlyItems(MaterialSymbols.Groups, diff.communities, R.string.backup_review_concord_body) + is CashuWalletDiff -> encryptedOnlyItems(MaterialSymbols.AccountBalanceWallet, diff.wallet, Res.string.backup_review_wallet_body) + is ConcordCommunityListDiff -> encryptedOnlyItems(MaterialSymbols.Groups, diff.communities, Res.string.backup_review_concord_body) else -> return false } return true @@ -193,15 +241,15 @@ private fun fateColor(fate: ItemFate): Color { private fun <T> LazyListScope.countTiles( items: List<Pair<T, ItemFate>>, - removedCaption: Int, - addedCaption: Int, + removedCaption: StringResource, + addedCaption: StringResource, ) { item(key = "count-tiles", contentType = "tiles") { val tones = conflictTones() Row(Pad.padding(top = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) { StatTile(items.count { it.second == ItemFate.DROPPED }.toString(), stringRes(removedCaption), tones.removed, Modifier.weight(1f)) StatTile(items.count { it.second == ItemFate.ADDED }.toString(), stringRes(addedCaption), tones.added, Modifier.weight(1f)) - StatTile(items.count { it.second == ItemFate.KEPT || it.second == ItemFate.CHANGED }.toString(), stringRes(R.string.backup_review_kept), tones.kept, Modifier.weight(1f)) + StatTile(items.count { it.second == ItemFate.KEPT || it.second == ItemFate.CHANGED }.toString(), stringRes(Res.string.backup_review_kept), tones.kept, Modifier.weight(1f)) } } } @@ -218,20 +266,20 @@ private fun LazyListScope.privateItemsCard(change: ContentChange) { /** How a relay list's rows are presented: its icon and what a kept or dropped relay is called. */ private class RelayFraming( val icon: MaterialSymbol, - val keptTag: Int, - val droppedTag: Int = R.string.backup_review_dropped_tag, + val keptTag: StringResource, + val droppedTag: StringResource = Res.string.backup_review_dropped_tag, ) private fun framingOf(type: BackupEventType): RelayFraming = when (type) { - BackupEventType.DM_RELAYS -> RelayFraming(MaterialSymbols.Mail, R.string.backup_review_receiving_tag) - BackupEventType.KEY_PACKAGE_RELAYS -> RelayFraming(MaterialSymbols.Key, R.string.backup_review_listed_tag) - BackupEventType.SEARCH_RELAYS -> RelayFraming(MaterialSymbols.Search, R.string.backup_review_searching_tag) - BackupEventType.INDEXER_RELAYS -> RelayFraming(MaterialSymbols.Dns, R.string.backup_review_listed_tag) - BackupEventType.RELAY_FEEDS -> RelayFraming(MaterialSymbols.CellTower, R.string.backup_review_listed_tag) - BackupEventType.PRIVATE_OUTBOX_RELAYS -> RelayFraming(MaterialSymbols.Lock, R.string.backup_review_storing_tag) - BackupEventType.TRUSTED_RELAYS -> RelayFraming(MaterialSymbols.Shield, R.string.backup_review_trusted_tag, R.string.backup_review_untrusted_tag) - else -> RelayFraming(MaterialSymbols.Dns, R.string.backup_review_listed_tag) + BackupEventType.DM_RELAYS -> RelayFraming(MaterialSymbols.Mail, Res.string.backup_review_receiving_tag) + BackupEventType.KEY_PACKAGE_RELAYS -> RelayFraming(MaterialSymbols.Key, Res.string.backup_review_listed_tag) + BackupEventType.SEARCH_RELAYS -> RelayFraming(MaterialSymbols.Search, Res.string.backup_review_searching_tag) + BackupEventType.INDEXER_RELAYS -> RelayFraming(MaterialSymbols.Dns, Res.string.backup_review_listed_tag) + BackupEventType.RELAY_FEEDS -> RelayFraming(MaterialSymbols.CellTower, Res.string.backup_review_listed_tag) + BackupEventType.PRIVATE_OUTBOX_RELAYS -> RelayFraming(MaterialSymbols.Lock, Res.string.backup_review_storing_tag) + BackupEventType.TRUSTED_RELAYS -> RelayFraming(MaterialSymbols.Shield, Res.string.backup_review_trusted_tag, Res.string.backup_review_untrusted_tag) + else -> RelayFraming(MaterialSymbols.Dns, Res.string.backup_review_listed_tag) } private fun savedRelaysOf(event: Event): List<NormalizedRelayUrl> = @@ -277,8 +325,8 @@ private fun RelayFateRow( when (fate) { ItemFate.KEPT -> framing.keptTag ItemFate.DROPPED -> framing.droppedTag - ItemFate.ADDED -> R.string.backup_review_new_tag - ItemFate.CHANGED -> R.string.backup_review_demoted_tag + ItemFate.ADDED -> Res.string.backup_review_new_tag + ItemFate.CHANGED -> Res.string.backup_review_demoted_tag } Row( Pad @@ -320,14 +368,14 @@ private fun LazyListScope.blockedRelayItems( Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp)) { Icon(symbol = MaterialSymbols.LockOpen, contentDescription = null, tint = tones.changed, modifier = Modifier.size(44.dp)) Column { - Text(stringRes(R.string.backup_review_unblocked, unblocked.size.toString()), fontSize = 30.sp, lineHeight = 32.sp, fontWeight = FontWeight.Bold, color = tones.changed) - Text(stringRes(R.string.backup_review_unblocked_explainer), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.placeholderText) + Text(stringRes(Res.string.backup_review_unblocked, unblocked.size.toString()), fontSize = 30.sp, lineHeight = 32.sp, fontWeight = FontWeight.Bold, color = tones.changed) + Text(stringRes(Res.string.backup_review_unblocked_explainer), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.placeholderText) } } } } if (unblocked.isNotEmpty()) { - item(key = "unblocked-caption", contentType = "caption") { SectionCaption(stringRes(R.string.backup_review_no_longer_blocked), Pad.padding(top = 18.dp)) } + item(key = "unblocked-caption", contentType = "caption") { SectionCaption(stringRes(Res.string.backup_review_no_longer_blocked), Pad.padding(top = 18.dp)) } items(unblocked, key = { "unblocked-" + it.url }, contentType = { "relay-row" }) { url -> val tones = conflictTones() Row( @@ -346,12 +394,12 @@ private fun LazyListScope.blockedRelayItems( Icon(symbol = MaterialSymbols.LockOpen, contentDescription = null, tint = tones.changed, modifier = Modifier.size(18.dp)) } Text(url.url.removePrefix("wss://").removeSuffix("/"), modifier = Modifier.weight(1f), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis) - StatusTag(stringRes(R.string.backup_review_allowed_tag), tones.changed) + StatusTag(stringRes(Res.string.backup_review_allowed_tag), tones.changed) } } } if (newlyBlocked.isNotEmpty()) { - item(key = "newly-blocked-caption", contentType = "caption") { SectionCaption(stringRes(R.string.backup_review_newly_blocked), Pad.padding(top = 18.dp)) } + item(key = "newly-blocked-caption", contentType = "caption") { SectionCaption(stringRes(Res.string.backup_review_newly_blocked), Pad.padding(top = 18.dp)) } items(newlyBlocked, key = { "newly-blocked-" + it.url }, contentType = { "relay-row" }) { url -> RelayFateRow(url, ItemFate.ADDED, framingOf(BackupEventType.BLOCKED_RELAYS), nav) } @@ -359,7 +407,7 @@ private fun LazyListScope.blockedRelayItems( if (stillBlocked.isNotEmpty()) { item(key = "still-blocked", contentType = "chips") { Column(Pad.padding(top = 18.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - SectionCaption(stringRes(R.string.backup_review_still_blocked)) + SectionCaption(stringRes(Res.string.backup_review_still_blocked)) FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { stillBlocked.forEach { url -> Row( @@ -392,7 +440,7 @@ private fun <T> LazyListScope.spaceGrid( key: (T) -> String, content: @Composable (T, ItemFate, Modifier) -> Unit, ) { - countTiles(items, R.string.backup_review_left, R.string.backup_review_joined) + countTiles(items, Res.string.backup_review_left, Res.string.backup_review_joined) items(items.chunked(2), key = { keyPrefix + key(it.first().first) }, contentType = { "space-row" }) { row -> Row(Pad.padding(top = 10.dp).fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) { row.forEach { (item, fate) -> content(item, fate, Modifier.weight(1f)) } @@ -436,9 +484,9 @@ private fun SpaceTile( } val tag = when (fate) { - ItemFate.DROPPED -> R.string.backup_review_left_tag to tones.removed - ItemFate.ADDED -> R.string.backup_review_new_tag to tones.added - ItemFate.CHANGED -> R.string.backup_review_moved_tag to tones.changed + ItemFate.DROPPED -> Res.string.backup_review_left_tag to tones.removed + ItemFate.ADDED -> Res.string.backup_review_new_tag to tones.added + ItemFate.CHANGED -> Res.string.backup_review_moved_tag to tones.changed ItemFate.KEPT -> null } tag?.let { (res, color) -> StatusTag(stringRes(res), color, Modifier.align(Alignment.TopEnd)) } @@ -459,7 +507,7 @@ private fun LazyListScope.publicChatItems( val current = state?.channel ?: channel SpaceTile( name = current.toBestDisplayName(), - subtitle = stringRes(R.string.backup_type_public_chats), + subtitle = stringRes(Res.string.backup_type_public_chats), robot = tag.eventId, picture = channel.profilePicture(), fate = fate, @@ -525,7 +573,7 @@ private fun AddressableSpaceTile( } SpaceTile( name = name ?: address.dTag.ifBlank { address.toValue().toShortDisplay() }, - subtitle = stringRes(if (address.kind == AppDefinitionEvent.KIND) R.string.backup_review_feed else R.string.backup_type_communities), + subtitle = stringRes(if (address.kind == AppDefinitionEvent.KIND) Res.string.backup_review_feed else Res.string.backup_type_communities), robot = address.toValue(), picture = picture, fate = fate, @@ -543,7 +591,7 @@ private fun LazyListScope.ephemeralRoomItems( ) { val saved = (conflict.saved as? EphemeralChatListEvent)?.publicRooms().orEmpty() val rooms = fates(saved, diff.rooms) { it } - countTiles(rooms, R.string.backup_review_left, R.string.backup_review_joined) + countTiles(rooms, Res.string.backup_review_left, Res.string.backup_review_joined) items(rooms, key = { "room-" + it.first.toKey() }, contentType = { "room-row" }) { (room, fate) -> RoomRow(room, fate, nav) } privateItemsCard(diff.privateItems) } @@ -582,8 +630,8 @@ private fun RoomRow( ) } when (fate) { - ItemFate.DROPPED -> StatusTag(stringRes(R.string.backup_review_left_tag), color) - ItemFate.ADDED -> StatusTag(stringRes(R.string.backup_review_new_tag), color) + ItemFate.DROPPED -> StatusTag(stringRes(Res.string.backup_review_left_tag), color) + ItemFate.ADDED -> StatusTag(stringRes(Res.string.backup_review_new_tag), color) else -> {} } } @@ -602,11 +650,11 @@ private fun LazyListScope.topicItems( nav: INav, ) { val topics = fates(saved, diff) { it.lowercase() } - countTiles(topics, if (isPlace) R.string.backup_review_places_gone else R.string.backup_review_topics_gone, R.string.backup_review_new_count_caption) + countTiles(topics, if (isPlace) Res.string.backup_review_places_gone else Res.string.backup_review_topics_gone, Res.string.backup_review_new_count_caption) item(key = "topic-cloud", contentType = "cloud") { val tones = conflictTones() Column(Pad.padding(top = 18.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { - SectionCaption(stringRes(if (isPlace) R.string.backup_review_places_you_follow else R.string.backup_review_hashtags_you_follow)) + SectionCaption(stringRes(if (isPlace) Res.string.backup_review_places_you_follow else Res.string.backup_review_hashtags_you_follow)) FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), itemVerticalAlignment = Alignment.CenterVertically) { topics.forEach { (topic, fate) -> val color = fateColor(fate) @@ -706,15 +754,15 @@ private fun LazyListScope.trustProviderItems( Icon(symbol = MaterialSymbols.Group, contentDescription = null, tint = tones.changed) } Column { - Text(stringRes(R.string.backup_review_services_changed, affected.toString(), services.size.toString()), fontSize = 24.sp, lineHeight = 28.sp, fontWeight = FontWeight.Bold) - Text(stringRes(R.string.backup_review_services_changed_explainer), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.placeholderText) + Text(stringRes(Res.string.backup_review_services_changed, affected.toString(), services.size.toString()), fontSize = 24.sp, lineHeight = 28.sp, fontWeight = FontWeight.Bold) + Text(stringRes(Res.string.backup_review_services_changed_explainer), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.placeholderText) } } } } item(key = "trust-header", contentType = "table-header") { Row(Pad.padding(top = 16.dp, bottom = 4.dp).padding(horizontal = 12.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) { - listOf(R.string.backup_review_service, R.string.backup_review_saved, R.string.backup_review_new).forEachIndexed { index, res -> + listOf(Res.string.backup_review_service, Res.string.backup_review_saved, Res.string.backup_review_new).forEachIndexed { index, res -> Text(stringRes(res).uppercase(), Modifier.weight(if (index == 0) 1.1f else 1f), style = MaterialTheme.typography.labelSmall, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.placeholderText) } } @@ -732,7 +780,7 @@ private fun LazyListScope.trustProviderItems( horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Icon(symbol = MaterialSymbols.Warning, contentDescription = null, tint = tones.changed, modifier = Modifier.size(18.dp)) - Text(stringRes(R.string.backup_review_trust_warning), style = MaterialTheme.typography.bodySmall, color = tones.changed) + Text(stringRes(Res.string.backup_review_trust_warning), style = MaterialTheme.typography.bodySmall, color = tones.changed) } } privateItemsCard(diff.privateItems) @@ -771,9 +819,9 @@ private fun ServiceTableRow( @Composable private fun serviceLabel(type: String): String = when (type) { - "rank" -> stringRes(R.string.backup_review_service_rank) - "followers" -> stringRes(R.string.backup_review_service_followers) - "t" -> stringRes(R.string.backup_review_service_topics) + "rank" -> stringRes(Res.string.backup_review_service_rank) + "followers" -> stringRes(Res.string.backup_review_service_followers) + "t" -> stringRes(Res.string.backup_review_service_topics) else -> type.replace('_', ' ').replaceFirstChar { it.uppercase() } } @@ -826,7 +874,7 @@ private fun LazyListScope.paymentTargetItems( ) { val saved = (conflict.saved as? PaymentTargetsEvent)?.paymentTargets().orEmpty() val targets = fates(saved, diff.targets) { it } - moneyHero(targets.count { it.second == ItemFate.DROPPED }, R.plurals.backup_review_payment_targets_gone) + moneyHero(targets.count { it.second == ItemFate.DROPPED }, Res.plurals.backup_review_payment_targets_gone) items(targets, key = { "target-" + it.first.type + ":" + it.first.authority }, contentType = { "money-row" }) { (target, fate) -> PaymentTargetRow(target, fate) } } @@ -836,15 +884,15 @@ private fun LazyListScope.offerItems( ) { val saved = (conflict.saved as? Bolt12OfferListEvent)?.offers().orEmpty() val offers = fates(saved, diff.offers) { it } - moneyHero(offers.count { it.second == ItemFate.DROPPED }, R.plurals.backup_review_offers_gone) + moneyHero(offers.count { it.second == ItemFate.DROPPED }, Res.plurals.backup_review_offers_gone) items(offers, key = { "offer-" + it.first }, contentType = { "money-row" }) { (offer, fate) -> - MoneyRow(stringRes(R.string.backup_review_offer), offer.toShortDisplay(prefixSize = 4), fate) + MoneyRow(stringRes(Res.string.backup_review_offer), offer.toShortDisplay(prefixSize = 4), fate) } } private fun LazyListScope.moneyHero( dropped: Int, - headline: Int, + headline: PluralStringResource, ) { item(key = "money-hero", contentType = "hero") { val tones = conflictTones() @@ -856,7 +904,7 @@ private fun LazyListScope.moneyHero( } Column { Text(pluralStringResource(headline, dropped, dropped), fontSize = 24.sp, lineHeight = 28.sp, fontWeight = FontWeight.Bold, color = color) - Text(stringRes(R.string.backup_review_payments_explainer), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.placeholderText) + Text(stringRes(Res.string.backup_review_payments_explainer), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.placeholderText) } } } @@ -902,8 +950,8 @@ private fun MoneyRow( ) } when (fate) { - ItemFate.DROPPED -> StatusTag(stringRes(R.string.backup_review_dropped_tag), color) - ItemFate.ADDED -> StatusTag(stringRes(R.string.backup_review_new_tag), color) + ItemFate.DROPPED -> StatusTag(stringRes(Res.string.backup_review_dropped_tag), color) + ItemFate.ADDED -> StatusTag(stringRes(Res.string.backup_review_new_tag), color) else -> {} } } @@ -916,15 +964,15 @@ private fun MoneyRow( private fun LazyListScope.encryptedOnlyItems( icon: MaterialSymbol, change: ContentChange, - body: Int, + body: StringResource, ) { item(key = "encrypted-hero", contentType = "hero") { val tones = conflictTones() val (color, headline) = when (change) { - ContentChange.CLEARED -> tones.removed to R.string.backup_review_encrypted_cleared - ContentChange.ADDED -> tones.added to R.string.backup_review_encrypted_added - else -> tones.changed to R.string.backup_review_encrypted_changed + ContentChange.CLEARED -> tones.removed to Res.string.backup_review_encrypted_cleared + ContentChange.ADDED -> tones.added to Res.string.backup_review_encrypted_added + else -> tones.changed to Res.string.backup_review_encrypted_changed } Column(Pad.padding(top = 8.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) { TintedPanel(color.copy(alpha = 0.12f)) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt index 58195a5d31..ff28dfa6c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt @@ -23,9 +23,98 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.backups import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalResources -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.backups.BackupEventType +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.backup_conflict_relay_read_only +import com.vitorpamplona.amethyst.commons.resources.backup_conflict_relay_read_write +import com.vitorpamplona.amethyst.commons.resources.backup_conflict_relay_write_only +import com.vitorpamplona.amethyst.commons.resources.backup_entry_algo_feed +import com.vitorpamplona.amethyst.commons.resources.backup_entry_chat_room +import com.vitorpamplona.amethyst.commons.resources.backup_entry_community +import com.vitorpamplona.amethyst.commons.resources.backup_entry_hashtag +import com.vitorpamplona.amethyst.commons.resources.backup_entry_identity_claim +import com.vitorpamplona.amethyst.commons.resources.backup_entry_location +import com.vitorpamplona.amethyst.commons.resources.backup_entry_mint +import com.vitorpamplona.amethyst.commons.resources.backup_entry_nutzap_key +import com.vitorpamplona.amethyst.commons.resources.backup_entry_offer +import com.vitorpamplona.amethyst.commons.resources.backup_entry_other +import com.vitorpamplona.amethyst.commons.resources.backup_entry_payment_target +import com.vitorpamplona.amethyst.commons.resources.backup_entry_person +import com.vitorpamplona.amethyst.commons.resources.backup_entry_profile_field +import com.vitorpamplona.amethyst.commons.resources.backup_entry_public_chat +import com.vitorpamplona.amethyst.commons.resources.backup_entry_relay +import com.vitorpamplona.amethyst.commons.resources.backup_entry_relay_group +import com.vitorpamplona.amethyst.commons.resources.backup_entry_thread +import com.vitorpamplona.amethyst.commons.resources.backup_entry_trust_provider +import com.vitorpamplona.amethyst.commons.resources.backup_entry_word +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_about +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_banner +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_birthday +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_bot +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_clink_offer +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_display_name +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_lud06 +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_lud16 +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_name +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_nip05 +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_picture +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_pronouns +import com.vitorpamplona.amethyst.commons.resources.backup_profile_field_website +import com.vitorpamplona.amethyst.commons.resources.backup_type_app_settings +import com.vitorpamplona.amethyst.commons.resources.backup_type_app_settings_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_blocked_relays +import com.vitorpamplona.amethyst.commons.resources.backup_type_blocked_relays_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_bolt12_offers +import com.vitorpamplona.amethyst.commons.resources.backup_type_bolt12_offers_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_cashu_wallet +import com.vitorpamplona.amethyst.commons.resources.backup_type_cashu_wallet_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_communities +import com.vitorpamplona.amethyst.commons.resources.backup_type_communities_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_concord_communities +import com.vitorpamplona.amethyst.commons.resources.backup_type_concord_communities_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_dm_relays +import com.vitorpamplona.amethyst.commons.resources.backup_type_dm_relays_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_ephemeral_chats +import com.vitorpamplona.amethyst.commons.resources.backup_type_ephemeral_chats_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_favorite_algo_feeds +import com.vitorpamplona.amethyst.commons.resources.backup_type_favorite_algo_feeds_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_follow_list +import com.vitorpamplona.amethyst.commons.resources.backup_type_follow_list_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_geohashes +import com.vitorpamplona.amethyst.commons.resources.backup_type_geohashes_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_hashtags +import com.vitorpamplona.amethyst.commons.resources.backup_type_hashtags_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_indexer_relays +import com.vitorpamplona.amethyst.commons.resources.backup_type_indexer_relays_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_key_package_relays +import com.vitorpamplona.amethyst.commons.resources.backup_type_key_package_relays_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_mute_list +import com.vitorpamplona.amethyst.commons.resources.backup_type_mute_list_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_nutzap_info +import com.vitorpamplona.amethyst.commons.resources.backup_type_nutzap_info_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_other +import com.vitorpamplona.amethyst.commons.resources.backup_type_other_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_outbox_inbox_relays +import com.vitorpamplona.amethyst.commons.resources.backup_type_outbox_inbox_relays_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_payment_targets +import com.vitorpamplona.amethyst.commons.resources.backup_type_payment_targets_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_private_outbox_relays +import com.vitorpamplona.amethyst.commons.resources.backup_type_private_outbox_relays_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_profile +import com.vitorpamplona.amethyst.commons.resources.backup_type_profile_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_public_chats +import com.vitorpamplona.amethyst.commons.resources.backup_type_public_chats_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_relay_feeds +import com.vitorpamplona.amethyst.commons.resources.backup_type_relay_feeds_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_relay_groups +import com.vitorpamplona.amethyst.commons.resources.backup_type_relay_groups_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_search_relays +import com.vitorpamplona.amethyst.commons.resources.backup_type_search_relays_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_trust_providers +import com.vitorpamplona.amethyst.commons.resources.backup_type_trust_providers_explainer +import com.vitorpamplona.amethyst.commons.resources.backup_type_trusted_relays +import com.vitorpamplona.amethyst.commons.resources.backup_type_trusted_relays_explainer +import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListDiff import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListDiff @@ -61,6 +150,7 @@ import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListDiff import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataDiff import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListDiff import com.vitorpamplona.quartz.nipB1Bolt12Zaps.offer.Bolt12OfferListDiff +import org.jetbrains.compose.resources.StringResource private const val MAX_VALUE_LENGTH = 80 @@ -168,7 +258,7 @@ sealed interface ReviewItem { /** One labelled group of differences (people, relays, profile fields…) of a conflict. */ @Immutable class DiffGroup( - val label: Int, + val label: StringResource, val removed: List<ReviewItem>, val added: List<ReviewItem>, val changed: List<ReviewItem>, @@ -189,7 +279,7 @@ class DiffPresentation( /** A group from a [ListDiff] of an event's parsed items. */ private fun <T> listGroup( - label: Int, + label: StringResource, diff: ListDiff<T>, item: (T) -> ReviewItem, change: (ItemChange<T>) -> ReviewItem, @@ -197,7 +287,7 @@ private fun <T> listGroup( /** A group whose items can't change, only be added or removed (hashtags, plain relays…). */ private fun <T> listGroup( - label: Int, + label: StringResource, diff: ListDiff<T>, item: (T) -> ReviewItem, ) = listGroup(label, diff, item) { item(it.after) } @@ -212,7 +302,7 @@ private inline fun <reified T> ListDiff<*>.only() = /** A group from single-valued fields: a field set only after is added, only before is removed. */ private fun fieldGroup( - label: Int, + label: StringResource, fields: List<Pair<String, ValueChange<String>?>>, ): DiffGroup { val removed = mutableListOf<ReviewItem>() @@ -242,14 +332,38 @@ private fun arrow( */ @Composable fun rememberPresentation(diff: EventDiff): DiffPresentation { - val resources = LocalResources.current - return remember(diff, resources) { presentationOf(diff) { resources.getString(it) } } + // Compose resources resolve only in composition (or suspended), so the few labels + // [presentationOf] writes into item text are resolved here and handed in. The map + // compares by value, so a recomposition with the same locale keeps the cached result. + val labels = PRESENTATION_LABELS.associateWith { stringRes(it) } + return remember(diff, labels) { presentationOf(diff) { labels.getValue(it) } } } +/** Every label [presentationOf] resolves through its `str` parameter. */ +private val PRESENTATION_LABELS = + listOf( + Res.string.backup_profile_field_name, + Res.string.backup_profile_field_display_name, + Res.string.backup_profile_field_about, + Res.string.backup_profile_field_picture, + Res.string.backup_profile_field_banner, + Res.string.backup_profile_field_website, + Res.string.backup_profile_field_nip05, + Res.string.backup_profile_field_lud16, + Res.string.backup_profile_field_lud06, + Res.string.backup_profile_field_clink_offer, + Res.string.backup_profile_field_pronouns, + Res.string.backup_profile_field_birthday, + Res.string.backup_profile_field_bot, + Res.string.backup_conflict_relay_read_write, + Res.string.backup_conflict_relay_read_only, + Res.string.backup_conflict_relay_write_only, + ) + /** Turns each event's own diff into typed, labelled groups. [str] resolves the labels. */ fun presentationOf( diff: EventDiff, - str: (Int) -> String, + str: (StringResource) -> String, ): DiffPresentation { val (groups, content) = when (diff) { @@ -258,31 +372,31 @@ fun presentationOf( val bot = diff.bot?.let { ValueChange(it.before?.toString(), it.after?.toString()) } listOf( fieldGroup( - R.string.backup_entry_profile_field, + Res.string.backup_entry_profile_field, listOf( - str(R.string.backup_profile_field_name) to diff.name, - str(R.string.backup_profile_field_display_name) to diff.displayName, - str(R.string.backup_profile_field_about) to diff.about, - str(R.string.backup_profile_field_picture) to diff.picture, - str(R.string.backup_profile_field_banner) to diff.banner, - str(R.string.backup_profile_field_website) to diff.website, - str(R.string.backup_profile_field_nip05) to diff.nip05, - str(R.string.backup_profile_field_lud16) to diff.lud16, - str(R.string.backup_profile_field_lud06) to diff.lud06, - str(R.string.backup_profile_field_clink_offer) to diff.clinkOffer, - str(R.string.backup_profile_field_pronouns) to diff.pronouns, - str(R.string.backup_profile_field_birthday) to birthday, - str(R.string.backup_profile_field_bot) to bot, + str(Res.string.backup_profile_field_name) to diff.name, + str(Res.string.backup_profile_field_display_name) to diff.displayName, + str(Res.string.backup_profile_field_about) to diff.about, + str(Res.string.backup_profile_field_picture) to diff.picture, + str(Res.string.backup_profile_field_banner) to diff.banner, + str(Res.string.backup_profile_field_website) to diff.website, + str(Res.string.backup_profile_field_nip05) to diff.nip05, + str(Res.string.backup_profile_field_lud16) to diff.lud16, + str(Res.string.backup_profile_field_lud06) to diff.lud06, + str(Res.string.backup_profile_field_clink_offer) to diff.clinkOffer, + str(Res.string.backup_profile_field_pronouns) to diff.pronouns, + str(Res.string.backup_profile_field_birthday) to birthday, + str(Res.string.backup_profile_field_bot) to bot, ), ), listGroup( - R.string.backup_entry_identity_claim, + Res.string.backup_entry_identity_claim, diff.identityClaims, { ReviewItem.Text(it.platformIdentity()) }, { ReviewItem.Text(it.after.platformIdentity(), arrow(it.before.proof, it.after.proof)) }, ), listGroup( - R.string.backup_entry_other, + Res.string.backup_entry_other, diff.otherFields, { ReviewItem.Text(it.first, clip(it.second)) }, { ReviewItem.Text(it.after.first, clip(it.before.second) + " → " + clip(it.after.second)) }, @@ -292,7 +406,7 @@ fun presentationOf( is ContactListDiff -> listOf( listGroup( - R.string.backup_entry_person, + Res.string.backup_entry_person, diff.follows, { ReviewItem.Person(it.pubKey, it.petname) }, { @@ -308,32 +422,32 @@ fun presentationOf( ) to ContentChange.NONE is MuteListDiff -> listOf( - listGroup(R.string.backup_entry_person, diff.publicMutes.only<UserTag>(), { ReviewItem.Person(it.pubKey) }), - listGroup(R.string.backup_entry_word, diff.publicMutes.only<WordTag>(), { ReviewItem.Text("\"" + it.word + "\"") }), - listGroup(R.string.backup_entry_hashtag, diff.publicMutes.only<HashtagTag>(), { ReviewItem.Text("#" + it.hashtag) }), - listGroup(R.string.backup_entry_thread, diff.publicMutes.only<EventTag>(), { ReviewItem.Thread(it.eventId) }), + listGroup(Res.string.backup_entry_person, diff.publicMutes.only<UserTag>(), { ReviewItem.Person(it.pubKey) }), + listGroup(Res.string.backup_entry_word, diff.publicMutes.only<WordTag>(), { ReviewItem.Text("\"" + it.word + "\"") }), + listGroup(Res.string.backup_entry_hashtag, diff.publicMutes.only<HashtagTag>(), { ReviewItem.Text("#" + it.hashtag) }), + listGroup(Res.string.backup_entry_thread, diff.publicMutes.only<EventTag>(), { ReviewItem.Thread(it.eventId) }), ) to diff.privateItems is AdvertisedRelayListDiff -> { val types = mapOf( - AdvertisedRelayType.BOTH to str(R.string.backup_conflict_relay_read_write), - AdvertisedRelayType.READ to str(R.string.backup_conflict_relay_read_only), - AdvertisedRelayType.WRITE to str(R.string.backup_conflict_relay_write_only), + AdvertisedRelayType.BOTH to str(Res.string.backup_conflict_relay_read_write), + AdvertisedRelayType.READ to str(Res.string.backup_conflict_relay_read_only), + AdvertisedRelayType.WRITE to str(Res.string.backup_conflict_relay_write_only), ) listOf( listGroup( - R.string.backup_entry_relay, + Res.string.backup_entry_relay, diff.relays, { ReviewItem.Relay(it.relayUrl, types[it.type]) }, { ReviewItem.Relay(it.after.relayUrl, arrow(types[it.before.type], types[it.after.type])) }, ), ) to ContentChange.NONE } - is RelayListDiff -> listOf(listGroup(R.string.backup_entry_relay, diff.relays, { ReviewItem.Relay(it) })) to diff.privateRelays + is RelayListDiff -> listOf(listGroup(Res.string.backup_entry_relay, diff.relays, { ReviewItem.Relay(it) })) to diff.privateRelays is ChannelListDiff -> listOf( listGroup( - R.string.backup_entry_public_chat, + Res.string.backup_entry_public_chat, diff.channels, { ReviewItem.PublicChat(it.eventId) }, { ReviewItem.PublicChat(it.after.eventId, arrow(it.before.relay?.url, it.after.relay?.url)) }, @@ -342,28 +456,28 @@ fun presentationOf( is CommunityListDiff -> listOf( listGroup( - R.string.backup_entry_community, + Res.string.backup_entry_community, diff.communities, { ReviewItem.Addressable(it.address) }, { ReviewItem.Addressable(it.after.address, arrow(it.before.relayHint?.url, it.after.relayHint?.url)) }, ), ) to diff.privateItems - is HashtagListDiff -> listOf(listGroup(R.string.backup_entry_hashtag, diff.hashtags, { ReviewItem.Text("#$it") })) to diff.privateItems - is GeohashListDiff -> listOf(listGroup(R.string.backup_entry_location, diff.geohashes, { ReviewItem.Text(it) })) to diff.privateItems + is HashtagListDiff -> listOf(listGroup(Res.string.backup_entry_hashtag, diff.hashtags, { ReviewItem.Text("#$it") })) to diff.privateItems + is GeohashListDiff -> listOf(listGroup(Res.string.backup_entry_location, diff.geohashes, { ReviewItem.Text(it) })) to diff.privateItems is FavoriteAlgoFeedsListDiff -> listOf( listGroup( - R.string.backup_entry_algo_feed, + Res.string.backup_entry_algo_feed, diff.feeds, { ReviewItem.Addressable(it.address) }, { ReviewItem.Addressable(it.after.address, arrow(it.before.relayHint?.url, it.after.relayHint?.url)) }, ), ) to diff.privateItems - is EphemeralChatListDiff -> listOf(listGroup(R.string.backup_entry_chat_room, diff.rooms, { ReviewItem.ChatRoom(it) })) to diff.privateItems + is EphemeralChatListDiff -> listOf(listGroup(Res.string.backup_entry_chat_room, diff.rooms, { ReviewItem.ChatRoom(it) })) to diff.privateItems is SimpleGroupListDiff -> listOf( listGroup( - R.string.backup_entry_relay_group, + Res.string.backup_entry_relay_group, diff.groups, { ReviewItem.Text(it.name ?: it.groupId, it.relayUrl) }, { ReviewItem.Text(it.after.name ?: it.after.groupId, arrow(it.before.name, it.after.name)) }, @@ -372,7 +486,7 @@ fun presentationOf( is TrustProviderListDiff -> listOf( listGroup( - R.string.backup_entry_trust_provider, + Res.string.backup_entry_trust_provider, diff.providers, { ReviewItem.Person(it.pubkey, it.service.type) }, { ReviewItem.Person(it.after.pubkey, it.after.service.type + ": " + arrow(it.before.relayUrl.url, it.after.relayUrl.url)) }, @@ -381,19 +495,19 @@ fun presentationOf( is NutzapInfoDiff -> listOf( listGroup( - R.string.backup_entry_mint, + Res.string.backup_entry_mint, diff.mints, { ReviewItem.Text(it.mintUrl, it.units.joinToString().ifEmpty { null }) }, { ReviewItem.Text(it.after.mintUrl, arrow(it.before.units.joinToString(), it.after.units.joinToString())) }, ), - listGroup(R.string.backup_entry_relay, diff.relays, { ReviewItem.Relay(it) }), + listGroup(Res.string.backup_entry_relay, diff.relays, { ReviewItem.Relay(it) }), nutzapKeyGroup(diff.p2pkPubkey), ) to ContentChange.NONE is PaymentTargetsDiff -> listOf( - listGroup(R.string.backup_entry_payment_target, diff.targets, { ReviewItem.Text(it.type, clip(it.authority)) }), + listGroup(Res.string.backup_entry_payment_target, diff.targets, { ReviewItem.Text(it.type, clip(it.authority)) }), ) to ContentChange.NONE - is Bolt12OfferListDiff -> listOf(listGroup(R.string.backup_entry_offer, diff.offers, { ReviewItem.Text(clip(it)) })) to ContentChange.NONE + is Bolt12OfferListDiff -> listOf(listGroup(Res.string.backup_entry_offer, diff.offers, { ReviewItem.Text(clip(it)) })) to ContentChange.NONE is CashuWalletDiff -> emptyList<DiffGroup>() to diff.wallet is ConcordCommunityListDiff -> emptyList<DiffGroup>() to diff.communities is AppSpecificDataDiff -> emptyList<DiffGroup>() to diff.data @@ -404,13 +518,13 @@ fun presentationOf( private fun nutzapKeyGroup(change: ValueChange<HexKey>?): DiffGroup { val none = emptyList<ReviewItem>() - if (change == null) return DiffGroup(R.string.backup_entry_nutzap_key, none, none, none) + if (change == null) return DiffGroup(Res.string.backup_entry_nutzap_key, none, none, none) val before = change.before val after = change.after return when { - before != null && after == null -> DiffGroup(R.string.backup_entry_nutzap_key, listOf(ReviewItem.Text(before)), none, none) - before == null && after != null -> DiffGroup(R.string.backup_entry_nutzap_key, none, listOf(ReviewItem.Text(after)), none) - else -> DiffGroup(R.string.backup_entry_nutzap_key, none, none, listOf(ReviewItem.Text(arrow(before, after)))) + before != null && after == null -> DiffGroup(Res.string.backup_entry_nutzap_key, listOf(ReviewItem.Text(before)), none, none) + before == null && after != null -> DiffGroup(Res.string.backup_entry_nutzap_key, none, listOf(ReviewItem.Text(after)), none) + else -> DiffGroup(Res.string.backup_entry_nutzap_key, none, none, listOf(ReviewItem.Text(arrow(before, after)))) } } @@ -422,64 +536,64 @@ fun clip(text: String?): String { return if (oneLine.length > MAX_VALUE_LENGTH) oneLine.take(MAX_VALUE_LENGTH) + "…" else oneLine } -fun eventTypeName(type: BackupEventType): Int = +fun eventTypeName(type: BackupEventType): StringResource = when (type) { - BackupEventType.PROFILE -> R.string.backup_type_profile - BackupEventType.FOLLOW_LIST -> R.string.backup_type_follow_list - BackupEventType.MUTE_LIST -> R.string.backup_type_mute_list - BackupEventType.OUTBOX_INBOX_RELAYS -> R.string.backup_type_outbox_inbox_relays - BackupEventType.DM_RELAYS -> R.string.backup_type_dm_relays - BackupEventType.KEY_PACKAGE_RELAYS -> R.string.backup_type_key_package_relays - BackupEventType.SEARCH_RELAYS -> R.string.backup_type_search_relays - BackupEventType.INDEXER_RELAYS -> R.string.backup_type_indexer_relays - BackupEventType.RELAY_FEEDS -> R.string.backup_type_relay_feeds - BackupEventType.BLOCKED_RELAYS -> R.string.backup_type_blocked_relays - BackupEventType.TRUSTED_RELAYS -> R.string.backup_type_trusted_relays - BackupEventType.PRIVATE_OUTBOX_RELAYS -> R.string.backup_type_private_outbox_relays - BackupEventType.APP_SETTINGS -> R.string.backup_type_app_settings - BackupEventType.PUBLIC_CHATS -> R.string.backup_type_public_chats - BackupEventType.COMMUNITIES -> R.string.backup_type_communities - BackupEventType.HASHTAGS -> R.string.backup_type_hashtags - BackupEventType.GEOHASHES -> R.string.backup_type_geohashes - BackupEventType.FAVORITE_ALGO_FEEDS -> R.string.backup_type_favorite_algo_feeds - BackupEventType.EPHEMERAL_CHATS -> R.string.backup_type_ephemeral_chats - BackupEventType.RELAY_GROUPS -> R.string.backup_type_relay_groups - BackupEventType.CONCORD_COMMUNITIES -> R.string.backup_type_concord_communities - BackupEventType.TRUST_PROVIDERS -> R.string.backup_type_trust_providers - BackupEventType.CASHU_WALLET -> R.string.backup_type_cashu_wallet - BackupEventType.NUTZAP_INFO -> R.string.backup_type_nutzap_info - BackupEventType.PAYMENT_TARGETS -> R.string.backup_type_payment_targets - BackupEventType.BOLT12_OFFERS -> R.string.backup_type_bolt12_offers - BackupEventType.OTHER -> R.string.backup_type_other + BackupEventType.PROFILE -> Res.string.backup_type_profile + BackupEventType.FOLLOW_LIST -> Res.string.backup_type_follow_list + BackupEventType.MUTE_LIST -> Res.string.backup_type_mute_list + BackupEventType.OUTBOX_INBOX_RELAYS -> Res.string.backup_type_outbox_inbox_relays + BackupEventType.DM_RELAYS -> Res.string.backup_type_dm_relays + BackupEventType.KEY_PACKAGE_RELAYS -> Res.string.backup_type_key_package_relays + BackupEventType.SEARCH_RELAYS -> Res.string.backup_type_search_relays + BackupEventType.INDEXER_RELAYS -> Res.string.backup_type_indexer_relays + BackupEventType.RELAY_FEEDS -> Res.string.backup_type_relay_feeds + BackupEventType.BLOCKED_RELAYS -> Res.string.backup_type_blocked_relays + BackupEventType.TRUSTED_RELAYS -> Res.string.backup_type_trusted_relays + BackupEventType.PRIVATE_OUTBOX_RELAYS -> Res.string.backup_type_private_outbox_relays + BackupEventType.APP_SETTINGS -> Res.string.backup_type_app_settings + BackupEventType.PUBLIC_CHATS -> Res.string.backup_type_public_chats + BackupEventType.COMMUNITIES -> Res.string.backup_type_communities + BackupEventType.HASHTAGS -> Res.string.backup_type_hashtags + BackupEventType.GEOHASHES -> Res.string.backup_type_geohashes + BackupEventType.FAVORITE_ALGO_FEEDS -> Res.string.backup_type_favorite_algo_feeds + BackupEventType.EPHEMERAL_CHATS -> Res.string.backup_type_ephemeral_chats + BackupEventType.RELAY_GROUPS -> Res.string.backup_type_relay_groups + BackupEventType.CONCORD_COMMUNITIES -> Res.string.backup_type_concord_communities + BackupEventType.TRUST_PROVIDERS -> Res.string.backup_type_trust_providers + BackupEventType.CASHU_WALLET -> Res.string.backup_type_cashu_wallet + BackupEventType.NUTZAP_INFO -> Res.string.backup_type_nutzap_info + BackupEventType.PAYMENT_TARGETS -> Res.string.backup_type_payment_targets + BackupEventType.BOLT12_OFFERS -> Res.string.backup_type_bolt12_offers + BackupEventType.OTHER -> Res.string.backup_type_other } -fun eventTypeExplainer(type: BackupEventType): Int = +fun eventTypeExplainer(type: BackupEventType): StringResource = when (type) { - BackupEventType.PROFILE -> R.string.backup_type_profile_explainer - BackupEventType.FOLLOW_LIST -> R.string.backup_type_follow_list_explainer - BackupEventType.MUTE_LIST -> R.string.backup_type_mute_list_explainer - BackupEventType.OUTBOX_INBOX_RELAYS -> R.string.backup_type_outbox_inbox_relays_explainer - BackupEventType.DM_RELAYS -> R.string.backup_type_dm_relays_explainer - BackupEventType.KEY_PACKAGE_RELAYS -> R.string.backup_type_key_package_relays_explainer - BackupEventType.SEARCH_RELAYS -> R.string.backup_type_search_relays_explainer - BackupEventType.INDEXER_RELAYS -> R.string.backup_type_indexer_relays_explainer - BackupEventType.RELAY_FEEDS -> R.string.backup_type_relay_feeds_explainer - BackupEventType.BLOCKED_RELAYS -> R.string.backup_type_blocked_relays_explainer - BackupEventType.TRUSTED_RELAYS -> R.string.backup_type_trusted_relays_explainer - BackupEventType.PRIVATE_OUTBOX_RELAYS -> R.string.backup_type_private_outbox_relays_explainer - BackupEventType.APP_SETTINGS -> R.string.backup_type_app_settings_explainer - BackupEventType.PUBLIC_CHATS -> R.string.backup_type_public_chats_explainer - BackupEventType.COMMUNITIES -> R.string.backup_type_communities_explainer - BackupEventType.HASHTAGS -> R.string.backup_type_hashtags_explainer - BackupEventType.GEOHASHES -> R.string.backup_type_geohashes_explainer - BackupEventType.FAVORITE_ALGO_FEEDS -> R.string.backup_type_favorite_algo_feeds_explainer - BackupEventType.EPHEMERAL_CHATS -> R.string.backup_type_ephemeral_chats_explainer - BackupEventType.RELAY_GROUPS -> R.string.backup_type_relay_groups_explainer - BackupEventType.CONCORD_COMMUNITIES -> R.string.backup_type_concord_communities_explainer - BackupEventType.TRUST_PROVIDERS -> R.string.backup_type_trust_providers_explainer - BackupEventType.CASHU_WALLET -> R.string.backup_type_cashu_wallet_explainer - BackupEventType.NUTZAP_INFO -> R.string.backup_type_nutzap_info_explainer - BackupEventType.PAYMENT_TARGETS -> R.string.backup_type_payment_targets_explainer - BackupEventType.BOLT12_OFFERS -> R.string.backup_type_bolt12_offers_explainer - BackupEventType.OTHER -> R.string.backup_type_other_explainer + BackupEventType.PROFILE -> Res.string.backup_type_profile_explainer + BackupEventType.FOLLOW_LIST -> Res.string.backup_type_follow_list_explainer + BackupEventType.MUTE_LIST -> Res.string.backup_type_mute_list_explainer + BackupEventType.OUTBOX_INBOX_RELAYS -> Res.string.backup_type_outbox_inbox_relays_explainer + BackupEventType.DM_RELAYS -> Res.string.backup_type_dm_relays_explainer + BackupEventType.KEY_PACKAGE_RELAYS -> Res.string.backup_type_key_package_relays_explainer + BackupEventType.SEARCH_RELAYS -> Res.string.backup_type_search_relays_explainer + BackupEventType.INDEXER_RELAYS -> Res.string.backup_type_indexer_relays_explainer + BackupEventType.RELAY_FEEDS -> Res.string.backup_type_relay_feeds_explainer + BackupEventType.BLOCKED_RELAYS -> Res.string.backup_type_blocked_relays_explainer + BackupEventType.TRUSTED_RELAYS -> Res.string.backup_type_trusted_relays_explainer + BackupEventType.PRIVATE_OUTBOX_RELAYS -> Res.string.backup_type_private_outbox_relays_explainer + BackupEventType.APP_SETTINGS -> Res.string.backup_type_app_settings_explainer + BackupEventType.PUBLIC_CHATS -> Res.string.backup_type_public_chats_explainer + BackupEventType.COMMUNITIES -> Res.string.backup_type_communities_explainer + BackupEventType.HASHTAGS -> Res.string.backup_type_hashtags_explainer + BackupEventType.GEOHASHES -> Res.string.backup_type_geohashes_explainer + BackupEventType.FAVORITE_ALGO_FEEDS -> Res.string.backup_type_favorite_algo_feeds_explainer + BackupEventType.EPHEMERAL_CHATS -> Res.string.backup_type_ephemeral_chats_explainer + BackupEventType.RELAY_GROUPS -> Res.string.backup_type_relay_groups_explainer + BackupEventType.CONCORD_COMMUNITIES -> Res.string.backup_type_concord_communities_explainer + BackupEventType.TRUST_PROVIDERS -> Res.string.backup_type_trust_providers_explainer + BackupEventType.CASHU_WALLET -> Res.string.backup_type_cashu_wallet_explainer + BackupEventType.NUTZAP_INFO -> Res.string.backup_type_nutzap_info_explainer + BackupEventType.PAYMENT_TARGETS -> Res.string.backup_type_payment_targets_explainer + BackupEventType.BOLT12_OFFERS -> Res.string.backup_type_bolt12_offers_explainer + BackupEventType.OTHER -> Res.string.backup_type_other_explainer } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt index 8b5d7b0150..6497b54846 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt @@ -59,11 +59,34 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.model.backups.BackupEventType import com.vitorpamplona.amethyst.commons.model.backups.ReplaceableBackupConflict import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.navigation.routeFor +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.backup_action_block_again +import com.vitorpamplona.amethyst.commons.resources.backup_action_keep_count +import com.vitorpamplona.amethyst.commons.resources.backup_action_rejoin +import com.vitorpamplona.amethyst.commons.resources.backup_action_remute +import com.vitorpamplona.amethyst.commons.resources.backup_action_restore_count +import com.vitorpamplona.amethyst.commons.resources.backup_action_restore_feeds +import com.vitorpamplona.amethyst.commons.resources.backup_action_restore_places +import com.vitorpamplona.amethyst.commons.resources.backup_action_restore_profile +import com.vitorpamplona.amethyst.commons.resources.backup_action_restore_providers +import com.vitorpamplona.amethyst.commons.resources.backup_action_restore_relays +import com.vitorpamplona.amethyst.commons.resources.backup_action_restore_topics +import com.vitorpamplona.amethyst.commons.resources.backup_action_restore_wallet +import com.vitorpamplona.amethyst.commons.resources.backup_conflict_added +import com.vitorpamplona.amethyst.commons.resources.backup_conflict_changed +import com.vitorpamplona.amethyst.commons.resources.backup_conflict_keep_new +import com.vitorpamplona.amethyst.commons.resources.backup_conflict_private_added +import com.vitorpamplona.amethyst.commons.resources.backup_conflict_private_changed +import com.vitorpamplona.amethyst.commons.resources.backup_conflict_private_cleared +import com.vitorpamplona.amethyst.commons.resources.backup_conflict_removed +import com.vitorpamplona.amethyst.commons.resources.backup_conflict_restore_mine +import com.vitorpamplona.amethyst.commons.resources.backup_review_changed_by_other_app +import com.vitorpamplona.amethyst.commons.resources.backup_review_title_cleared +import com.vitorpamplona.amethyst.commons.resources.backup_review_title_updated import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton @@ -97,22 +120,23 @@ import com.vitorpamplona.quartz.nip60Cashu.wallet.CashuWalletDiff import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListDiff import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListDiff import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListDiff +import org.jetbrains.compose.resources.StringResource /** A row of the review list, flattened so hundreds of entries scroll lazily. */ @Immutable internal sealed interface ReviewRow { class Section( - val titleRes: Int, + val titleRes: StringResource, val count: Int, val tone: Tone, ) : ReviewRow class Note( - val textRes: Int, + val textRes: StringResource, ) : ReviewRow class Group( - val labelRes: Int, + val labelRes: StringResource, ) : ReviewRow class Entry( @@ -124,15 +148,15 @@ internal enum class Tone { REMOVED, ADDED, CHANGED } internal fun buildRows( presentation: DiffPresentation, - skipGroups: Set<Int> = emptySet(), + skipGroups: Set<StringResource> = emptySet(), ): List<ReviewRow> { val rows = mutableListOf<ReviewRow>() fun section( - titleRes: Int, + titleRes: StringResource, count: Int, tone: Tone, - contentNote: Int?, + contentNote: StringResource?, items: (DiffGroup) -> List<ReviewItem>, ) { if (count == 0 && contentNote == null) return @@ -149,9 +173,9 @@ internal fun buildRows( } val content = presentation.content - section(R.string.backup_conflict_removed, presentation.removedCount, Tone.REMOVED, R.string.backup_conflict_private_cleared.takeIf { content == ContentChange.CLEARED }) { it.removed } - section(R.string.backup_conflict_added, presentation.addedCount, Tone.ADDED, R.string.backup_conflict_private_added.takeIf { content == ContentChange.ADDED }) { it.added } - section(R.string.backup_conflict_changed, presentation.changedCount, Tone.CHANGED, R.string.backup_conflict_private_changed.takeIf { content == ContentChange.CHANGED }) { it.changed } + section(Res.string.backup_conflict_removed, presentation.removedCount, Tone.REMOVED, Res.string.backup_conflict_private_cleared.takeIf { content == ContentChange.CLEARED }) { it.removed } + section(Res.string.backup_conflict_added, presentation.addedCount, Tone.ADDED, Res.string.backup_conflict_private_added.takeIf { content == ContentChange.ADDED }) { it.added } + section(Res.string.backup_conflict_changed, presentation.changedCount, Tone.CHANGED, Res.string.backup_conflict_private_changed.takeIf { content == ContentChange.CHANGED }) { it.changed } return rows } @@ -268,7 +292,7 @@ internal fun LazyListScope.genericDiffItems( } @Composable -internal fun GroupLabel(labelRes: Int) { +internal fun GroupLabel(labelRes: StringResource) { Text( text = stringRes(labelRes), style = MaterialTheme.typography.labelLarge, @@ -339,7 +363,7 @@ internal fun eventTitle(conflict: ReplaceableBackupConflict): String { val name = stringRes(eventTypeName(conflict.eventType)).replaceFirstChar { it.uppercase() } // Says what happened to it, not just what it is: "Community list updated". val emptied = conflict.incoming.tags.isEmpty() && conflict.incoming.content.isEmpty() - return stringRes(if (emptied) R.string.backup_review_title_cleared else R.string.backup_review_title_updated, name) + return stringRes(if (emptied) Res.string.backup_review_title_cleared else Res.string.backup_review_title_updated, name) } /** The two choices, worded for what they do to this event: "Keep 120" / "Restore 523". */ @@ -348,48 +372,48 @@ private fun actionLabels(conflict: ReplaceableBackupConflict): Pair<String, Stri when (val diff = conflict.diff) { is ContactListDiff -> { val (saved, new) = conflict.followCounts - stringRes(R.string.backup_action_keep_count, new.toString()) to stringRes(R.string.backup_action_restore_count, saved.toString()) + stringRes(Res.string.backup_action_keep_count, new.toString()) to stringRes(Res.string.backup_action_restore_count, saved.toString()) } is MuteListDiff -> { val removed = diff.publicMutes.removed.size - stringRes(R.string.backup_conflict_keep_new) to - if (removed > 0) stringRes(R.string.backup_action_remute, removed.toString()) else stringRes(R.string.backup_conflict_restore_mine) + stringRes(Res.string.backup_conflict_keep_new) to + if (removed > 0) stringRes(Res.string.backup_action_remute, removed.toString()) else stringRes(Res.string.backup_conflict_restore_mine) } is SimpleGroupListDiff -> { val removed = diff.groups.removed.size - stringRes(R.string.backup_conflict_keep_new) to - if (removed > 0) stringRes(R.string.backup_action_rejoin, removed.toString()) else stringRes(R.string.backup_conflict_restore_mine) + stringRes(Res.string.backup_conflict_keep_new) to + if (removed > 0) stringRes(Res.string.backup_action_rejoin, removed.toString()) else stringRes(Res.string.backup_conflict_restore_mine) } - is MetadataDiff -> stringRes(R.string.backup_conflict_keep_new) to stringRes(R.string.backup_action_restore_profile) + is MetadataDiff -> stringRes(Res.string.backup_conflict_keep_new) to stringRes(Res.string.backup_action_restore_profile) is RelayListDiff -> if (conflict.eventType == BackupEventType.BLOCKED_RELAYS && diff.relays.removed.isNotEmpty()) { - stringRes(R.string.backup_conflict_keep_new) to + stringRes(Res.string.backup_conflict_keep_new) to stringRes( - R.string.backup_action_block_again, + Res.string.backup_action_block_again, diff.relays.removed.size .toString(), ) } else { - stringRes(R.string.backup_conflict_keep_new) to stringRes(R.string.backup_action_restore_relays) + stringRes(Res.string.backup_conflict_keep_new) to stringRes(Res.string.backup_action_restore_relays) } - is AdvertisedRelayListDiff -> stringRes(R.string.backup_conflict_keep_new) to stringRes(R.string.backup_action_restore_relays) + is AdvertisedRelayListDiff -> stringRes(Res.string.backup_conflict_keep_new) to stringRes(Res.string.backup_action_restore_relays) is ChannelListDiff -> rejoinLabels(diff.channels.removed.size) is CommunityListDiff -> rejoinLabels(diff.communities.removed.size) is EphemeralChatListDiff -> rejoinLabels(diff.rooms.removed.size) - is FavoriteAlgoFeedsListDiff -> stringRes(R.string.backup_conflict_keep_new) to stringRes(R.string.backup_action_restore_feeds) - is HashtagListDiff -> stringRes(R.string.backup_conflict_keep_new) to stringRes(R.string.backup_action_restore_topics) + is FavoriteAlgoFeedsListDiff -> stringRes(Res.string.backup_conflict_keep_new) to stringRes(Res.string.backup_action_restore_feeds) + is HashtagListDiff -> stringRes(Res.string.backup_conflict_keep_new) to stringRes(Res.string.backup_action_restore_topics) // Geohashes are places, not topics: the screen counts "places gone" under "Followed // locations", so the button has to agree with the page it sits on. - is GeohashListDiff -> stringRes(R.string.backup_conflict_keep_new) to stringRes(R.string.backup_action_restore_places) - is TrustProviderListDiff -> stringRes(R.string.backup_conflict_keep_new) to stringRes(R.string.backup_action_restore_providers) - is CashuWalletDiff -> stringRes(R.string.backup_conflict_keep_new) to stringRes(R.string.backup_action_restore_wallet) - else -> stringRes(R.string.backup_conflict_keep_new) to stringRes(R.string.backup_conflict_restore_mine) + is GeohashListDiff -> stringRes(Res.string.backup_conflict_keep_new) to stringRes(Res.string.backup_action_restore_places) + is TrustProviderListDiff -> stringRes(Res.string.backup_conflict_keep_new) to stringRes(Res.string.backup_action_restore_providers) + is CashuWalletDiff -> stringRes(Res.string.backup_conflict_keep_new) to stringRes(Res.string.backup_action_restore_wallet) + else -> stringRes(Res.string.backup_conflict_keep_new) to stringRes(Res.string.backup_conflict_restore_mine) } @Composable private fun rejoinLabels(removed: Int): Pair<String, String> = - stringRes(R.string.backup_conflict_keep_new) to - if (removed > 0) stringRes(R.string.backup_action_rejoin, removed.toString()) else stringRes(R.string.backup_conflict_restore_mine) + stringRes(Res.string.backup_conflict_keep_new) to + if (removed > 0) stringRes(Res.string.backup_action_rejoin, removed.toString()) else stringRes(Res.string.backup_conflict_restore_mine) /** When the other app changed it, and what the event is for. */ @Composable @@ -399,7 +423,7 @@ private fun BackupConflictIntro( ) { Column(modifier, verticalArrangement = Arrangement.spacedBy(2.dp)) { Text( - text = stringRes(R.string.backup_review_changed_by_other_app, timeAgoNoDot(conflict.cause.createdAt)), + text = stringRes(Res.string.backup_review_changed_by_other_app, timeAgoNoDot(conflict.cause.createdAt)), style = MaterialTheme.typography.labelLarge, color = conflictTones().removed, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnComposer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnComposer.kt index 0e19b487cb..c892f3d020 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnComposer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnComposer.kt @@ -42,12 +42,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.chats.ui.ThinSendButton import com.vitorpamplona.amethyst.commons.model.cordnGroups.CordnGroupChatroom import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.cordn_composer_hint import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldBorder @@ -233,7 +234,7 @@ internal fun CordnComposer( shape = EditFieldBorder, placeholder = { Text( - text = stringRes(R.string.cordn_composer_hint), + text = stringRes(Res.string.cordn_composer_hint), color = MaterialTheme.colorScheme.placeholderText, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnCreateGroupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnCreateGroupScreen.kt index 6deedde2d4..0b78b0ea5e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnCreateGroupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnCreateGroupScreen.kt @@ -55,11 +55,9 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.cordn.CoordinatorConfig import com.vitorpamplona.amethyst.commons.cordn.DiscoveredCoordinator import com.vitorpamplona.amethyst.commons.cordn.GroupExposure @@ -69,6 +67,55 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.back +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_discover_failed +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_discover_none +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_discover_seen +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_discover_seen_on +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_discover_unheard +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_hide_older +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_relays_more +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_show_all +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_show_fewer +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_show_older +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_stale_note +import com.vitorpamplona.amethyst.commons.resources.cordn_create_action +import com.vitorpamplona.amethyst.commons.resources.cordn_create_action_invite +import com.vitorpamplona.amethyst.commons.resources.cordn_create_admin_count +import com.vitorpamplona.amethyst.commons.resources.cordn_create_admin_only_me_on +import com.vitorpamplona.amethyst.commons.resources.cordn_create_coordinator_change +import com.vitorpamplona.amethyst.commons.resources.cordn_create_coordinator_discover +import com.vitorpamplona.amethyst.commons.resources.cordn_create_coordinator_hide +import com.vitorpamplona.amethyst.commons.resources.cordn_create_coordinator_new +import com.vitorpamplona.amethyst.commons.resources.cordn_create_coordinator_pubkey +import com.vitorpamplona.amethyst.commons.resources.cordn_create_coordinator_relays +import com.vitorpamplona.amethyst.commons.resources.cordn_create_coverage_all +import com.vitorpamplona.amethyst.commons.resources.cordn_create_coverage_partial +import com.vitorpamplona.amethyst.commons.resources.cordn_create_coverage_unknown +import com.vitorpamplona.amethyst.commons.resources.cordn_create_description +import com.vitorpamplona.amethyst.commons.resources.cordn_create_egalitarian +import com.vitorpamplona.amethyst.commons.resources.cordn_create_egalitarian_note +import com.vitorpamplona.amethyst.commons.resources.cordn_create_explainer +import com.vitorpamplona.amethyst.commons.resources.cordn_create_failed +import com.vitorpamplona.amethyst.commons.resources.cordn_create_name +import com.vitorpamplona.amethyst.commons.resources.cordn_create_no_coordinator +import com.vitorpamplona.amethyst.commons.resources.cordn_create_outcome_explainer +import com.vitorpamplona.amethyst.commons.resources.cordn_create_outcome_failed +import com.vitorpamplona.amethyst.commons.resources.cordn_create_outcome_link +import com.vitorpamplona.amethyst.commons.resources.cordn_create_outcome_no_welcome +import com.vitorpamplona.amethyst.commons.resources.cordn_create_outcome_open +import com.vitorpamplona.amethyst.commons.resources.cordn_create_outcome_skipped +import com.vitorpamplona.amethyst.commons.resources.cordn_create_outcome_title +import com.vitorpamplona.amethyst.commons.resources.cordn_create_outcome_waiting +import com.vitorpamplona.amethyst.commons.resources.cordn_create_people_none +import com.vitorpamplona.amethyst.commons.resources.cordn_create_step_admins +import com.vitorpamplona.amethyst.commons.resources.cordn_create_step_name +import com.vitorpamplona.amethyst.commons.resources.cordn_create_step_people +import com.vitorpamplona.amethyst.commons.resources.cordn_create_step_where +import com.vitorpamplona.amethyst.commons.resources.cordn_create_title +import com.vitorpamplona.amethyst.commons.resources.cordn_create_unreachable_count +import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable +import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable_detail +import com.vitorpamplona.amethyst.commons.resources.cordn_member_count import com.vitorpamplona.amethyst.commons.ui.components.EmptyState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.model.cordn.CordnCoverage @@ -131,7 +178,7 @@ fun CordnCreateGroupScreen( Icon(MaterialSymbols.AutoMirrored.ArrowBack, contentDescription = stringRes(Res.string.back)) } }, - title = { Text(stringRes(R.string.cordn_create_title)) }, + title = { Text(stringRes(Res.string.cordn_create_title)) }, ) }, ) { padding -> @@ -139,8 +186,8 @@ fun CordnCreateGroupScreen( // The app's own empty state, centred and titled, rather than a // sentence stranded in the top-left corner. EmptyState( - title = stringRes(R.string.cordn_group_unavailable), - description = stringRes(R.string.cordn_group_unavailable_detail), + title = stringRes(Res.string.cordn_group_unavailable), + description = stringRes(Res.string.cordn_group_unavailable_detail), modifier = Modifier.padding(padding), ) return@Scaffold @@ -164,14 +211,14 @@ fun CordnCreateGroupScreen( val draft = rememberCordnGroupDraft(accountViewModel) var creation by remember { mutableStateOf<CordnGroupCreation?>(null) } var error by remember { mutableStateOf<String?>(null) } - val failureFallback = stringRes(R.string.cordn_create_failed) + val failureFallback = stringRes(Res.string.cordn_create_failed) var busy by remember { mutableStateOf(false) } // Coordinators this account has never used, from their CEP-6 // announcements. Not added to the account by looking: picking one here // is what commits to it, and `createGroup` opens the session. var discovering by remember { mutableStateOf(false) } - val discoverFailed = stringRes(R.string.cordn_coordinators_discover_failed) + val discoverFailed = stringRes(Res.string.cordn_coordinators_discover_failed) // Offers this account already holds are not offers; they are the choices // above, and listing them twice would let the same coordinator be picked @@ -222,29 +269,29 @@ fun CordnCreateGroupScreen( verticalArrangement = Arrangement.spacedBy(12.dp), ) { Text( - text = stringRes(R.string.cordn_create_explainer), + text = stringRes(Res.string.cordn_create_explainer), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) - StepHeading(1, stringRes(R.string.cordn_create_step_name)) + StepHeading(1, stringRes(Res.string.cordn_create_step_name)) OutlinedTextField( value = draft.name, onValueChange = { draft.name = it }, - label = { Text(stringRes(R.string.cordn_create_name)) }, + label = { Text(stringRes(Res.string.cordn_create_name)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), ) OutlinedTextField( value = draft.description, onValueChange = { draft.description = it }, - label = { Text(stringRes(R.string.cordn_create_description)) }, + label = { Text(stringRes(Res.string.cordn_create_description)) }, singleLine = false, modifier = Modifier.fillMaxWidth(), ) - StepHeading(2, stringRes(R.string.cordn_create_step_people)) + StepHeading(2, stringRes(Res.string.cordn_create_step_people)) RosterSummary( roster = draft.roster, @@ -254,7 +301,7 @@ fun CordnCreateGroupScreen( nav = nav, ) - StepHeading(3, stringRes(R.string.cordn_create_step_where)) + StepHeading(3, stringRes(Res.string.cordn_create_step_where)) // Collapsed to the one coordinator that will be used, because with a // draft.roster in hand there is usually nothing left to decide. The whole @@ -326,9 +373,9 @@ fun CordnCreateGroupScreen( TextButton(onClick = { draft.showAllLive = !draft.showAllLive }) { Text( if (draft.showAllLive) { - stringRes(R.string.cordn_coordinators_show_fewer) + stringRes(Res.string.cordn_coordinators_show_fewer) } else { - stringRes(R.string.cordn_coordinators_show_all, live.size) + stringRes(Res.string.cordn_coordinators_show_all, live.size) }, ) } @@ -338,9 +385,9 @@ fun CordnCreateGroupScreen( TextButton(onClick = { draft.showStale = !draft.showStale }) { Text( if (draft.showStale) { - stringRes(R.string.cordn_coordinators_hide_older) + stringRes(Res.string.cordn_coordinators_hide_older) } else { - stringRes(R.string.cordn_coordinators_show_older, stale.size) + stringRes(Res.string.cordn_coordinators_show_older, stale.size) }, ) } @@ -348,7 +395,7 @@ fun CordnCreateGroupScreen( if (draft.showStale && stale.isNotEmpty()) { Text( - text = stringRes(R.string.cordn_coordinators_stale_note), + text = stringRes(Res.string.cordn_coordinators_stale_note), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -372,7 +419,7 @@ fun CordnCreateGroupScreen( } CoordinatorChoice( - label = stringRes(R.string.cordn_create_coordinator_new), + label = stringRes(Res.string.cordn_create_coordinator_new), pubKey = null, selected = draft.selected == null, onSelect = { @@ -402,20 +449,20 @@ fun CordnCreateGroupScreen( }, enabled = !discovering && !busy, ) { - Text(stringRes(R.string.cordn_create_coordinator_discover)) + Text(stringRes(Res.string.cordn_create_coordinator_discover)) } draft.discovered?.takeIf { offers.isEmpty() }?.let { result -> Text( text = if (result.unreachable.isEmpty()) { - stringRes(R.string.cordn_coordinators_discover_none) + stringRes(Res.string.cordn_coordinators_discover_none) } else { // "Nobody is announcing" and "we were not told" are // different answers and only one of them is final. // Reporting the first for the second sends someone // off to paste a pubkey by hand over a timeout. - stringRes(R.string.cordn_coordinators_discover_unheard, result.unreachable.size) + stringRes(Res.string.cordn_coordinators_discover_unheard, result.unreachable.size) }, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -433,7 +480,7 @@ fun CordnCreateGroupScreen( draft.userPicked = true error = null }, - label = { Text(stringRes(R.string.cordn_create_coordinator_pubkey)) }, + label = { Text(stringRes(Res.string.cordn_create_coordinator_pubkey)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), ) @@ -444,7 +491,7 @@ fun CordnCreateGroupScreen( draft.userPicked = true error = null }, - label = { Text(stringRes(R.string.cordn_create_coordinator_relays)) }, + label = { Text(stringRes(Res.string.cordn_create_coordinator_relays)) }, // A coordinator has no address beyond its pubkey (§8.5), so // the relays are how it is reached and more than one is // ordinary. One per line rather than comma-separated, @@ -456,7 +503,7 @@ fun CordnCreateGroupScreen( } } - StepHeading(4, stringRes(R.string.cordn_create_step_admins)) + StepHeading(4, stringRes(Res.string.cordn_create_step_admins)) // spec/01.md §5.3: leaving the admin list empty is not "set it up // later", it is choosing draft.egalitarian permanently -- so it is offered @@ -472,15 +519,15 @@ fun CordnCreateGroupScreen( horizontalArrangement = Arrangement.spacedBy(12.dp), ) { Column(Modifier.weight(1f)) { - Text(stringRes(R.string.cordn_create_egalitarian), style = MaterialTheme.typography.bodyMedium) + Text(stringRes(Res.string.cordn_create_egalitarian), style = MaterialTheme.typography.bodyMedium) Text( text = if (draft.egalitarian) { - stringRes(R.string.cordn_create_egalitarian_note) + stringRes(Res.string.cordn_create_egalitarian_note) } else if (draft.coAdmins.isEmpty()) { - stringRes(R.string.cordn_create_admin_only_me_on) + stringRes(Res.string.cordn_create_admin_only_me_on) } else { - pluralStringRes(LocalContext.current, R.plurals.cordn_create_admin_count, draft.coAdmins.size + 1, draft.coAdmins.size + 1) + pluralStringRes(Res.plurals.cordn_create_admin_count, draft.coAdmins.size + 1, draft.coAdmins.size + 1) }, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -561,9 +608,9 @@ fun CordnCreateGroupScreen( ) { Text( if (draft.roster.isEmpty()) { - stringRes(R.string.cordn_create_action) + stringRes(Res.string.cordn_create_action) } else { - pluralStringRes(LocalContext.current, R.plurals.cordn_create_action_invite, draft.roster.size, draft.roster.size) + pluralStringRes(Res.plurals.cordn_create_action_invite, draft.roster.size, draft.roster.size) }, ) } @@ -691,9 +738,9 @@ private fun resolvedName( @Composable private fun announcedLabel(announcedAt: Long): String = if (TimeUtils.now() - announcedAt > TimeUtils.ONE_MONTH) { - stringRes(R.string.cordn_coordinators_discover_seen_on, timeAgoNoDot(announcedAt).trim()) + stringRes(Res.string.cordn_coordinators_discover_seen_on, timeAgoNoDot(announcedAt).trim()) } else { - stringRes(R.string.cordn_coordinators_discover_seen, timeAgoNoDot(announcedAt).trim()) + stringRes(Res.string.cordn_coordinators_discover_seen, timeAgoNoDot(announcedAt).trim()) } /** The hosts it answers on, the first two and a count of the rest. */ @@ -705,7 +752,7 @@ private fun relayLabel(relays: List<NormalizedRelayUrl>): String? { return if (hosts.size <= 2) { shown } else { - stringRes(R.string.cordn_coordinators_relays_more, shown, hosts.size - 2) + stringRes(Res.string.cordn_coordinators_relays_more, shown, hosts.size - 2) } } @@ -791,7 +838,7 @@ private fun RosterSummary( ) { if (roster.isEmpty()) { Text( - text = stringRes(R.string.cordn_create_people_none), + text = stringRes(Res.string.cordn_create_people_none), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f), @@ -804,7 +851,7 @@ private fun RosterSummary( } Column(Modifier.weight(1f)) { Text( - text = pluralStringRes(LocalContext.current, R.plurals.cordn_member_count, roster.size + 1, roster.size + 1), + text = pluralStringRes(Res.plurals.cordn_member_count, roster.size + 1, roster.size + 1), style = MaterialTheme.typography.bodyMedium, ) // Counted over coordinators that answered, so an unreachable one @@ -812,7 +859,7 @@ private fun RosterSummary( val unreachable = roster.count { member -> coverage.values.none { it.answered && member in it.reachable } } if (unreachable > 0 && coverage.values.any { it.answered }) { Text( - text = pluralStringRes(LocalContext.current, R.plurals.cordn_create_unreachable_count, unreachable, unreachable), + text = pluralStringRes(Res.plurals.cordn_create_unreachable_count, unreachable, unreachable), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.error, ) @@ -855,7 +902,7 @@ private fun CoordinatorSummary( ) { if (config == null) { Text( - text = stringRes(R.string.cordn_create_no_coordinator), + text = stringRes(Res.string.cordn_create_no_coordinator), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f), @@ -879,9 +926,9 @@ private fun CoordinatorSummary( Text( text = when { - coverage == null || !coverage.answered -> stringRes(R.string.cordn_create_coverage_unknown) - reached == rosterSize -> stringRes(R.string.cordn_create_coverage_all, rosterSize) - else -> stringRes(R.string.cordn_create_coverage_partial, reached ?: 0, rosterSize) + coverage == null || !coverage.answered -> stringRes(Res.string.cordn_create_coverage_unknown) + reached == rosterSize -> stringRes(Res.string.cordn_create_coverage_all, rosterSize) + else -> stringRes(Res.string.cordn_create_coverage_partial, reached ?: 0, rosterSize) }, style = MaterialTheme.typography.labelSmall, color = @@ -899,9 +946,9 @@ private fun CoordinatorSummary( TextButton(onClick = onToggle) { Text( if (expanded) { - stringRes(R.string.cordn_create_coordinator_hide) + stringRes(Res.string.cordn_create_coordinator_hide) } else { - stringRes(R.string.cordn_create_coordinator_change) + stringRes(Res.string.cordn_create_coordinator_change) }, ) } @@ -934,11 +981,11 @@ private fun CordnCreationOutcome( AlertDialog( onDismissRequest = onDismiss, - title = { Text(stringRes(R.string.cordn_create_outcome_title)) }, + title = { Text(stringRes(Res.string.cordn_create_outcome_title)) }, text = { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Text( - text = stringRes(R.string.cordn_create_outcome_explainer), + text = stringRes(Res.string.cordn_create_outcome_explainer), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -960,10 +1007,10 @@ private fun CordnCreationOutcome( Text( text = when { - outcome == null -> stringRes(R.string.cordn_create_outcome_skipped) - outcome.sent -> stringRes(R.string.cordn_create_outcome_waiting) - outcome.joinedWithoutWelcome -> stringRes(R.string.cordn_create_outcome_no_welcome) - else -> outcome.failure?.message ?: stringRes(R.string.cordn_create_outcome_failed) + outcome == null -> stringRes(Res.string.cordn_create_outcome_skipped) + outcome.sent -> stringRes(Res.string.cordn_create_outcome_waiting) + outcome.joinedWithoutWelcome -> stringRes(Res.string.cordn_create_outcome_no_welcome) + else -> outcome.failure?.message ?: stringRes(Res.string.cordn_create_outcome_failed) }, style = MaterialTheme.typography.labelSmall, color = @@ -979,12 +1026,12 @@ private fun CordnCreationOutcome( // open themselves -- which needs the group to exist, // and now it does. if (outcome == null) { - TextButton(onClick = open) { Text(stringRes(R.string.cordn_create_outcome_link)) } + TextButton(onClick = open) { Text(stringRes(Res.string.cordn_create_outcome_link)) } } } } } }, - confirmButton = { TextButton(onClick = open) { Text(stringRes(R.string.cordn_create_outcome_open)) } }, + confirmButton = { TextButton(onClick = open) { Text(stringRes(Res.string.cordn_create_outcome_open)) } }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnCreateMembersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnCreateMembersScreen.kt index e80829bf90..1a09a1c890 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnCreateMembersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnCreateMembersScreen.kt @@ -47,15 +47,31 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentActivity import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.cordn_create_add_member +import com.vitorpamplona.amethyst.commons.resources.cordn_create_admin +import com.vitorpamplona.amethyst.commons.resources.cordn_create_people_done +import com.vitorpamplona.amethyst.commons.resources.cordn_create_people_empty +import com.vitorpamplona.amethyst.commons.resources.cordn_create_people_note +import com.vitorpamplona.amethyst.commons.resources.cordn_create_reach_count +import com.vitorpamplona.amethyst.commons.resources.cordn_create_reach_none +import com.vitorpamplona.amethyst.commons.resources.cordn_create_reach_unknown +import com.vitorpamplona.amethyst.commons.resources.cordn_create_step_people +import com.vitorpamplona.amethyst.commons.resources.cordn_create_you +import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable +import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable_detail +import com.vitorpamplona.amethyst.commons.resources.cordn_info_add_member_none +import com.vitorpamplona.amethyst.commons.resources.cordn_info_add_member_placeholder +import com.vitorpamplona.amethyst.commons.resources.cordn_info_has_key_package +import com.vitorpamplona.amethyst.commons.resources.cordn_info_remove_member +import com.vitorpamplona.amethyst.commons.resources.cordn_member_count import com.vitorpamplona.amethyst.commons.ui.components.EmptyState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton @@ -101,12 +117,12 @@ fun CordnCreateMembersScreen( val draft = rememberCordnGroupDraft(accountViewModel) Scaffold( - topBar = { TopBarWithBackButton(stringRes(R.string.cordn_create_step_people), nav) }, + topBar = { TopBarWithBackButton(stringRes(Res.string.cordn_create_step_people), nav) }, ) { padding -> if (runtime == null) { EmptyState( - title = stringRes(R.string.cordn_group_unavailable), - description = stringRes(R.string.cordn_group_unavailable_detail), + title = stringRes(Res.string.cordn_group_unavailable), + description = stringRes(Res.string.cordn_group_unavailable_detail), modifier = Modifier.padding(padding), ) return@Scaffold @@ -137,7 +153,7 @@ fun CordnCreateMembersScreen( verticalArrangement = Arrangement.spacedBy(12.dp), ) { Text( - text = stringRes(R.string.cordn_create_people_note), + text = stringRes(Res.string.cordn_create_people_note), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -148,8 +164,8 @@ fun CordnCreateMembersScreen( search = it if (it.length > 2) userSuggestions.processCurrentWord(it) else userSuggestions.reset() }, - label = { Text(stringRes(R.string.cordn_create_add_member)) }, - placeholder = { Text(stringRes(R.string.cordn_info_add_member_placeholder)) }, + label = { Text(stringRes(Res.string.cordn_create_add_member)) }, + placeholder = { Text(stringRes(Res.string.cordn_info_add_member_placeholder)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), ) @@ -168,7 +184,7 @@ fun CordnCreateMembersScreen( modifier = SuggestionListDefaultHeightChat, onEmpty = { Text( - text = stringRes(R.string.cordn_info_add_member_none), + text = stringRes(Res.string.cordn_info_add_member_none), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), @@ -182,7 +198,7 @@ fun CordnCreateMembersScreen( if (user.pubkeyHex in reachable) { Icon( symbol = MaterialSymbols.Key, - contentDescription = stringRes(R.string.cordn_info_has_key_package), + contentDescription = stringRes(Res.string.cordn_info_has_key_package), modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.primary, ) @@ -194,7 +210,7 @@ fun CordnCreateMembersScreen( }) { Icon( symbol = MaterialSymbols.PersonAdd, - contentDescription = stringRes(R.string.cordn_create_add_member), + contentDescription = stringRes(Res.string.cordn_create_add_member), tint = MaterialTheme.colorScheme.primary, ) } @@ -206,7 +222,7 @@ fun CordnCreateMembersScreen( HorizontalDivider() Text( - text = pluralStringRes(LocalContext.current, R.plurals.cordn_member_count, draft.roster.size + 1, draft.roster.size + 1), + text = pluralStringRes(Res.plurals.cordn_member_count, draft.roster.size + 1, draft.roster.size + 1), style = MaterialTheme.typography.titleSmall, ) @@ -230,7 +246,7 @@ fun CordnCreateMembersScreen( if (draft.roster.isEmpty()) { Text( - text = stringRes(R.string.cordn_create_people_empty), + text = stringRes(Res.string.cordn_create_people_empty), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -240,7 +256,7 @@ fun CordnCreateMembersScreen( onClick = { nav.popBack() }, modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), ) { - Text(stringRes(R.string.cordn_create_people_done)) + Text(stringRes(Res.string.cordn_create_people_done)) } } } @@ -263,14 +279,14 @@ private fun CreatorRow( Column(Modifier.weight(1f)) { Text(observeUserNameByHex(me, accountViewModel), style = MaterialTheme.typography.bodyMedium) Text( - text = stringRes(R.string.cordn_create_you), + text = stringRes(Res.string.cordn_create_you), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } if (!egalitarian) { Text( - text = stringRes(R.string.cordn_create_admin), + text = stringRes(Res.string.cordn_create_admin), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary, ) @@ -304,9 +320,9 @@ private fun MemberRow( Text( text = when { - answered == 0 -> stringRes(R.string.cordn_create_reach_unknown) - reaching > 0 -> pluralStringRes(LocalContext.current, R.plurals.cordn_create_reach_count, reaching, reaching) - else -> stringRes(R.string.cordn_create_reach_none) + answered == 0 -> stringRes(Res.string.cordn_create_reach_unknown) + reaching > 0 -> pluralStringRes(Res.plurals.cordn_create_reach_count, reaching, reaching) + else -> stringRes(Res.string.cordn_create_reach_none) }, style = MaterialTheme.typography.labelSmall, color = @@ -323,7 +339,7 @@ private fun MemberRow( if (!egalitarian) { TextButton(onClick = onToggleAdmin) { Text( - text = stringRes(R.string.cordn_create_admin), + text = stringRes(Res.string.cordn_create_admin), style = MaterialTheme.typography.labelMedium, color = if (isAdmin) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -333,7 +349,7 @@ private fun MemberRow( IconButton(onClick = onRemove) { Icon( symbol = MaterialSymbols.PersonRemove, - contentDescription = stringRes(R.string.cordn_info_remove_member), + contentDescription = stringRes(Res.string.cordn_info_remove_member), modifier = Modifier.size(20.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt index 7e0e2d7a13..b884f56dbd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt @@ -69,7 +69,6 @@ import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.cordn.CordnGroupManager import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -78,10 +77,33 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.back import com.vitorpamplona.amethyst.commons.resources.cancel +import com.vitorpamplona.amethyst.commons.resources.cordn_action_editing +import com.vitorpamplona.amethyst.commons.resources.cordn_action_unpin +import com.vitorpamplona.amethyst.commons.resources.cordn_chat_empty_description +import com.vitorpamplona.amethyst.commons.resources.cordn_chat_empty_title +import com.vitorpamplona.amethyst.commons.resources.cordn_group_info +import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable import com.vitorpamplona.amethyst.commons.resources.cordn_group_untitled +import com.vitorpamplona.amethyst.commons.resources.cordn_media_download_failed +import com.vitorpamplona.amethyst.commons.resources.cordn_media_no_server +import com.vitorpamplona.amethyst.commons.resources.cordn_media_unreadable +import com.vitorpamplona.amethyst.commons.resources.cordn_media_upload_failed +import com.vitorpamplona.amethyst.commons.resources.cordn_member_count +import com.vitorpamplona.amethyst.commons.resources.cordn_message_deleted +import com.vitorpamplona.amethyst.commons.resources.cordn_pinned_by +import com.vitorpamplona.amethyst.commons.resources.cordn_pinned_count +import com.vitorpamplona.amethyst.commons.resources.cordn_pinned_next +import com.vitorpamplona.amethyst.commons.resources.cordn_pinned_previous +import com.vitorpamplona.amethyst.commons.resources.cordn_pinned_show_all +import com.vitorpamplona.amethyst.commons.resources.cordn_pinned_title +import com.vitorpamplona.amethyst.commons.resources.cordn_send_failed +import com.vitorpamplona.amethyst.commons.resources.cordn_send_no_session +import com.vitorpamplona.amethyst.commons.resources.cordn_voice_record +import com.vitorpamplona.amethyst.commons.resources.cordn_voice_stop import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.EncryptedMediaUrlVideo import com.vitorpamplona.amethyst.commons.ui.components.EmptyState +import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText @@ -152,7 +174,7 @@ fun CordnGroupChatScreen( // (it was forgotten, or never opened) and not an error to throw at the // user as a blank screen. Column(Modifier.fillMaxSize().padding(24.dp), verticalArrangement = Arrangement.Center) { - Text(stringRes(R.string.cordn_group_unavailable), style = MaterialTheme.typography.bodyLarge) + Text(stringRes(Res.string.cordn_group_unavailable), style = MaterialTheme.typography.bodyLarge) } return } @@ -209,9 +231,9 @@ private fun CordnGroupChat( // and simply never arrived. var sendError by remember { mutableStateOf<String?>(null) } val context = LocalContext.current - val uploadFailed = stringRes(R.string.cordn_media_upload_failed) - val sendFailed = stringRes(R.string.cordn_send_failed) - val noSession = stringRes(R.string.cordn_send_no_session) + val uploadFailed = stringRes(Res.string.cordn_media_upload_failed) + val sendFailed = stringRes(Res.string.cordn_send_failed) + val noSession = stringRes(Res.string.cordn_send_no_session) fun manager() = accountViewModel.account.cordnRuntime @@ -359,8 +381,8 @@ private fun CordnGroupChat( // action that actually needed it. if (rows.isEmpty()) { EmptyState( - title = stringRes(R.string.cordn_chat_empty_title), - description = stringRes(R.string.cordn_chat_empty_description), + title = stringRes(Res.string.cordn_chat_empty_title), + description = stringRes(Res.string.cordn_chat_empty_description), modifier = Modifier.weight(1f), ) } @@ -548,7 +570,7 @@ private fun CordnGroupChat( } if (editing != null) { ComposerBanner( - label = stringRes(R.string.cordn_action_editing), + label = stringRes(Res.string.cordn_action_editing), onCancel = { editing = null room.draft.value = "" @@ -712,7 +734,7 @@ private fun PinnedRibbon( IconButton(onClick = { index = (at - 1 + pinned.size) % pinned.size }, modifier = Modifier.size(28.dp)) { Icon( symbol = MaterialSymbols.AutoMirrored.KeyboardArrowLeft, - contentDescription = stringRes(R.string.cordn_pinned_previous), + contentDescription = stringRes(Res.string.cordn_pinned_previous), modifier = Modifier.size(18.dp), ) } @@ -724,7 +746,7 @@ private fun PinnedRibbon( IconButton(onClick = { index = (at + 1) % pinned.size }, modifier = Modifier.size(28.dp)) { Icon( symbol = MaterialSymbols.AutoMirrored.KeyboardArrowRight, - contentDescription = stringRes(R.string.cordn_pinned_next), + contentDescription = stringRes(Res.string.cordn_pinned_next), modifier = Modifier.size(18.dp), ) } @@ -733,7 +755,7 @@ private fun PinnedRibbon( IconButton(onClick = { showingAll = true }, modifier = Modifier.size(28.dp)) { Icon( symbol = MaterialSymbols.AutoMirrored.List, - contentDescription = stringRes(R.string.cordn_pinned_show_all), + contentDescription = stringRes(Res.string.cordn_pinned_show_all), modifier = Modifier.size(18.dp), ) } @@ -786,7 +808,7 @@ private fun PinnedLine( // points at. text = if (annotations.isDeleted(message.envelope.id)) { - stringRes(R.string.cordn_message_deleted) + stringRes(Res.string.cordn_message_deleted) } else { annotations.contentOf(message.envelope.id).orEmpty() }, @@ -822,7 +844,7 @@ private fun AllPinnedDialog( Row(verticalAlignment = Alignment.CenterVertically) { Icon(MaterialSymbols.PushPin, contentDescription = null, modifier = Modifier.size(18.dp)) Text( - text = stringRes(R.string.cordn_pinned_title), + text = stringRes(Res.string.cordn_pinned_title), modifier = Modifier.padding(start = 8.dp), ) } @@ -830,7 +852,7 @@ private fun AllPinnedDialog( text = { Column { Text( - text = pluralStringRes(LocalContext.current, R.plurals.cordn_pinned_count, pinned.size, pinned.size), + text = pluralStringRes(Res.plurals.cordn_pinned_count, pinned.size, pinned.size), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -861,7 +883,7 @@ private fun AllPinnedDialog( modifier = Modifier.weight(1f).padding(start = 8.dp), ) TextButton(onClick = { onUnpin(message) }) { - Text(stringRes(R.string.cordn_action_unpin), style = MaterialTheme.typography.labelSmall) + Text(stringRes(Res.string.cordn_action_unpin), style = MaterialTheme.typography.labelSmall) } } @@ -869,7 +891,7 @@ private fun AllPinnedDialog( Text( text = stringRes( - R.string.cordn_pinned_by, + Res.string.cordn_pinned_by, observeUserNameByHex(pinnedBy, accountViewModel), ), style = MaterialTheme.typography.labelSmall, @@ -880,7 +902,7 @@ private fun AllPinnedDialog( Text( text = if (annotations.isDeleted(message.envelope.id)) { - stringRes(R.string.cordn_message_deleted) + stringRes(Res.string.cordn_message_deleted) } else { annotations.contentOf(message.envelope.id).orEmpty() }, @@ -944,7 +966,7 @@ private fun CordnChatTopBar( Text(title, maxLines = 1, overflow = TextOverflow.Ellipsis) if (members.isNotEmpty()) { Text( - text = pluralStringRes(LocalContext.current, R.plurals.cordn_member_count, members.size, members.size), + text = pluralStringRes(Res.plurals.cordn_member_count, members.size, members.size), style = MaterialTheme.typography.bodySmall, ) } @@ -963,7 +985,7 @@ private fun CordnChatTopBar( // GroupAdd rather than Info, as in Marmot: the same screen, and the thing // people come to it for is adding someone. IconButton(onClick = onInfo) { - Icon(MaterialSymbols.GroupAdd, contentDescription = stringRes(R.string.cordn_group_info)) + Icon(MaterialSymbols.GroupAdd, contentDescription = stringRes(Res.string.cordn_group_info)) } }, ) @@ -995,10 +1017,10 @@ private suspend fun sendAttachment( // a reason a person can act on. val session = accountViewModel.account.cordnRuntime?.sessionOrNull(room.coordinatorPubKey) - ?: throw CordnAttachmentException(stringRes(context, R.string.cordn_send_no_session)) + ?: throw CordnAttachmentException(loadStringRes(Res.string.cordn_send_no_session)) val group = session.manager.group(room.gid) - ?: throw CordnAttachmentException(stringRes(context, R.string.cordn_send_no_session)) + ?: throw CordnAttachmentException(loadStringRes(Res.string.cordn_send_no_session)) // The gallery inside the dialog can delete what was picked, which leaves an empty // orchestrator rather than a null one. canPost() now refuses that, but indexing it @@ -1052,14 +1074,14 @@ private suspend fun sendAttachment( val name = withContext(Dispatchers.IO) { resolveDisplayName(context, uri) } val bytes = withContext(Dispatchers.IO) { context.contentResolver.openInputStream(finalUri)?.use { it.readBytes() } } - ?: throw CordnAttachmentException(stringRes(context, R.string.cordn_media_unreadable)) + ?: throw CordnAttachmentException(loadStringRes(Res.string.cordn_media_unreadable)) // Null means the chosen host has no base URL, which is a setting the person can // change — the one failure here that is entirely actionable. val tag = CordnMediaService(accountViewModel.account) .upload(group, bytes, mime, name, context, state.selectedServer.baseUrl) - ?: throw CordnAttachmentException(stringRes(context, R.string.cordn_media_no_server)) + ?: throw CordnAttachmentException(loadStringRes(Res.string.cordn_media_no_server)) // Into the room as well, for the same reason every other send is: an // attachment of your own echoes back as an Echo and would otherwise be @@ -1140,7 +1162,7 @@ internal fun CordnAttachment( if (mediaKey == null) { Text( - text = stringRes(R.string.cordn_media_download_failed), + text = stringRes(Res.string.cordn_media_download_failed), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.error, ) @@ -1254,7 +1276,7 @@ internal fun VoiceNoteButton( ) { Icon( symbol = if (recording) MaterialSymbols.Stop else MaterialSymbols.Mic, - contentDescription = stringRes(if (recording) R.string.cordn_voice_stop else R.string.cordn_voice_record), + contentDescription = stringRes(if (recording) Res.string.cordn_voice_stop else Res.string.cordn_voice_record), // Matches the attach icon beside it; red only while recording, // which is the one state worth pulling the eye. tint = if (recording) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.placeholderText, @@ -1284,10 +1306,10 @@ private suspend fun sendVoiceNote( // note that goes nowhere and says nothing is the same bug twice. val session = accountViewModel.account.cordnRuntime?.sessionOrNull(room.coordinatorPubKey) - ?: throw CordnAttachmentException(stringRes(context, R.string.cordn_send_no_session)) + ?: throw CordnAttachmentException(loadStringRes(Res.string.cordn_send_no_session)) val group = session.manager.group(room.gid) - ?: throw CordnAttachmentException(stringRes(context, R.string.cordn_send_no_session)) + ?: throw CordnAttachmentException(loadStringRes(Res.string.cordn_send_no_session)) try { val bytes = withContext(Dispatchers.IO) { recording.file.readBytes() } @@ -1304,7 +1326,7 @@ private suspend fun sendVoiceNote( // is what makes the bubble match the preview. waveform = recording.amplitudes, ) - ?: throw CordnAttachmentException(stringRes(context, R.string.cordn_media_no_server)) + ?: throw CordnAttachmentException(loadStringRes(Res.string.cordn_media_no_server)) // Into the room as well, for the same reason every other send is: an // attachment of your own echoes back as an Echo and would otherwise be // invisible to the person who sent it. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupInfoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupInfoScreen.kt index 25c90c354e..b08e44b341 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupInfoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupInfoScreen.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup -import android.content.Context import android.util.Log import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.clickable @@ -58,12 +57,10 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString 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.commons.cordn.CordnGroupException import com.vitorpamplona.amethyst.commons.cordn.GroupExposure import com.vitorpamplona.amethyst.commons.cordn.ui.CordnExposureCard @@ -76,8 +73,58 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.back import com.vitorpamplona.amethyst.commons.resources.cancel import com.vitorpamplona.amethyst.commons.resources.copy_npub_to_clipboard +import com.vitorpamplona.amethyst.commons.resources.cordn_admin_action_failed +import com.vitorpamplona.amethyst.commons.resources.cordn_admin_coordinator_silent +import com.vitorpamplona.amethyst.commons.resources.cordn_admin_no_key_package +import com.vitorpamplona.amethyst.commons.resources.cordn_admin_no_self_remove +import com.vitorpamplona.amethyst.commons.resources.cordn_admin_no_welcome +import com.vitorpamplona.amethyst.commons.resources.cordn_admin_not_a_member +import com.vitorpamplona.amethyst.commons.resources.cordn_admin_someone +import com.vitorpamplona.amethyst.commons.resources.cordn_admin_wrong_key_package +import com.vitorpamplona.amethyst.commons.resources.cordn_create_description +import com.vitorpamplona.amethyst.commons.resources.cordn_create_name +import com.vitorpamplona.amethyst.commons.resources.cordn_exposure_close +import com.vitorpamplona.amethyst.commons.resources.cordn_exposure_open +import com.vitorpamplona.amethyst.commons.resources.cordn_group_info +import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable +import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable_detail import com.vitorpamplona.amethyst.commons.resources.cordn_group_untitled +import com.vitorpamplona.amethyst.commons.resources.cordn_info_add_member +import com.vitorpamplona.amethyst.commons.resources.cordn_info_add_member_none +import com.vitorpamplona.amethyst.commons.resources.cordn_info_add_member_note +import com.vitorpamplona.amethyst.commons.resources.cordn_info_add_member_placeholder +import com.vitorpamplona.amethyst.commons.resources.cordn_info_admin_badge +import com.vitorpamplona.amethyst.commons.resources.cordn_info_admins_kept +import com.vitorpamplona.amethyst.commons.resources.cordn_info_coordinator +import com.vitorpamplona.amethyst.commons.resources.cordn_info_coordinator_key +import com.vitorpamplona.amethyst.commons.resources.cordn_info_coordinator_nprofile +import com.vitorpamplona.amethyst.commons.resources.cordn_info_copy_nprofile +import com.vitorpamplona.amethyst.commons.resources.cordn_info_edit_details +import com.vitorpamplona.amethyst.commons.resources.cordn_info_egalitarian +import com.vitorpamplona.amethyst.commons.resources.cordn_info_epoch +import com.vitorpamplona.amethyst.commons.resources.cordn_info_gid +import com.vitorpamplona.amethyst.commons.resources.cordn_info_has_key_package +import com.vitorpamplona.amethyst.commons.resources.cordn_info_members +import com.vitorpamplona.amethyst.commons.resources.cordn_info_remove_confirm_body +import com.vitorpamplona.amethyst.commons.resources.cordn_info_remove_confirm_title +import com.vitorpamplona.amethyst.commons.resources.cordn_info_remove_member +import com.vitorpamplona.amethyst.commons.resources.cordn_info_save +import com.vitorpamplona.amethyst.commons.resources.cordn_info_technical +import com.vitorpamplona.amethyst.commons.resources.cordn_member_count +import com.vitorpamplona.amethyst.commons.resources.cordn_requests_accept +import com.vitorpamplona.amethyst.commons.resources.cordn_requests_admin_only +import com.vitorpamplona.amethyst.commons.resources.cordn_requests_check +import com.vitorpamplona.amethyst.commons.resources.cordn_requests_decline +import com.vitorpamplona.amethyst.commons.resources.cordn_requests_failed +import com.vitorpamplona.amethyst.commons.resources.cordn_requests_none +import com.vitorpamplona.amethyst.commons.resources.cordn_requests_title +import com.vitorpamplona.amethyst.commons.resources.cordn_send_no_session +import com.vitorpamplona.amethyst.commons.resources.cordn_share_copied +import com.vitorpamplona.amethyst.commons.resources.cordn_share_copy +import com.vitorpamplona.amethyst.commons.resources.cordn_share_explainer +import com.vitorpamplona.amethyst.commons.resources.cordn_share_title import com.vitorpamplona.amethyst.commons.ui.components.EmptyState +import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.util.toShortDisplay @@ -124,8 +171,8 @@ fun CordnGroupInfoScreen( // The app's own empty state, centred and titled, rather than a // sentence stranded in the top-left corner. EmptyState( - title = stringRes(R.string.cordn_group_unavailable), - description = stringRes(R.string.cordn_group_unavailable_detail), + title = stringRes(Res.string.cordn_group_unavailable), + description = stringRes(Res.string.cordn_group_unavailable_detail), modifier = Modifier.padding(padding), ) } @@ -151,7 +198,7 @@ private fun InfoTopBar( Icon(MaterialSymbols.AutoMirrored.ArrowBack, contentDescription = stringRes(Res.string.back)) } }, - title = { Text(stringRes(R.string.cordn_group_info)) }, + title = { Text(stringRes(Res.string.cordn_group_info)) }, actions = actions, ) } @@ -208,8 +255,8 @@ private fun CordnGroupInfo( DisposableEffect(Unit) { onDispose { userSuggestions.reset() } } - val failed = stringRes(R.string.cordn_admin_action_failed) - val noSession = stringRes(R.string.cordn_send_no_session) + val failed = stringRes(Res.string.cordn_admin_action_failed) + val noSession = stringRes(Res.string.cordn_send_no_session) // Computed, never asserted: hardcoding these made the card look like a // disclosure while reporting the same four values for every group. @@ -236,7 +283,6 @@ private fun CordnGroupInfo( * roster, and a rename stayed the old name. The runtime's own methods pair * each commit with that refresh. */ - val context = LocalContext.current fun runAdmin( who: String? = null, @@ -257,7 +303,7 @@ private fun CordnGroupInfo( // and gids in full, so the screen that had just shown a face // answered with 64 hex characters. Log.w("CordnGroupInfo", "admin action failed in ${room.gid}: ${e.message}", e) - adminError = adminFailureText(context, e, who, failed) + adminError = adminFailureText(e, who, failed) } finally { busy = false } @@ -274,7 +320,7 @@ private fun CordnGroupInfo( IconButton(onClick = { renaming = true }, enabled = !busy) { Icon( symbol = MaterialSymbols.Edit, - contentDescription = stringRes(R.string.cordn_info_edit_details), + contentDescription = stringRes(Res.string.cordn_info_edit_details), ) } } @@ -303,7 +349,7 @@ private fun CordnGroupInfo( } // A subtitle, not a labelled row above a list that is itself the count. Text( - text = pluralStringRes(LocalContext.current, R.plurals.cordn_member_count, members.size, members.size), + text = pluralStringRes(Res.plurals.cordn_member_count, members.size, members.size), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = 4.dp), @@ -321,7 +367,7 @@ private fun CordnGroupInfo( // ContextVM addresses it with ordinary p-tags, so it has a kind 0 like // anyone else and the app can already resolve it to a name, an avatar // and a profile to tap through to. - Text(stringRes(R.string.cordn_info_coordinator), style = MaterialTheme.typography.titleMedium) + Text(stringRes(Res.string.cordn_info_coordinator), style = MaterialTheme.typography.titleMedium) Row( Modifier.fillMaxWidth().padding(top = 8.dp), @@ -347,7 +393,7 @@ private fun CordnGroupInfo( IconButton(onClick = { showExposure = true }, enabled = exposure != null) { Icon( symbol = MaterialSymbols.Info, - contentDescription = stringRes(R.string.cordn_exposure_open), + contentDescription = stringRes(Res.string.cordn_exposure_open), tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } @@ -355,7 +401,7 @@ private fun CordnGroupInfo( HorizontalDivider(Modifier.padding(vertical = 16.dp)) - Text(stringRes(R.string.cordn_info_members), style = MaterialTheme.typography.titleMedium) + Text(stringRes(Res.string.cordn_info_members), style = MaterialTheme.typography.titleMedium) // Said once, and only when it is true. An empty admin set is not "none // configured" -- spec/01.md makes it permanently egalitarian, so the @@ -364,7 +410,7 @@ private fun CordnGroupInfo( // a count of them adds nothing. if (admins.isEmpty()) { Text( - text = stringRes(R.string.cordn_info_egalitarian), + text = stringRes(Res.string.cordn_info_egalitarian), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = 2.dp, bottom = 4.dp), @@ -385,7 +431,7 @@ private fun CordnGroupInfo( ) if (member in admins) { Text( - text = stringRes(R.string.cordn_info_admin_badge), + text = stringRes(Res.string.cordn_info_admin_badge), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary, ) @@ -398,7 +444,7 @@ private fun CordnGroupInfo( IconButton(onClick = { removing = member }, enabled = !busy) { Icon( symbol = MaterialSymbols.PersonRemove, - contentDescription = stringRes(R.string.cordn_info_remove_member), + contentDescription = stringRes(Res.string.cordn_info_remove_member), modifier = Modifier.size(20.dp), tint = MaterialTheme.colorScheme.error, ) @@ -476,9 +522,9 @@ private fun CordnGroupInfo( // would be a setting nobody should be nudged into. AlertDialog( onDismissRequest = { removing = null }, - title = { Text(stringRes(R.string.cordn_info_remove_confirm_title)) }, + title = { Text(stringRes(Res.string.cordn_info_remove_confirm_title)) }, text = { - Text(stringRes(R.string.cordn_info_remove_confirm_body, removingName)) + Text(stringRes(Res.string.cordn_info_remove_confirm_body, removingName)) }, confirmButton = { TextButton(onClick = { @@ -486,7 +532,7 @@ private fun CordnGroupInfo( runAdmin(removingName) { it.removeMember(coordinatorPubKey, room.gid, target) } }) { Text( - text = stringRes(R.string.cordn_info_remove_member), + text = stringRes(Res.string.cordn_info_remove_member), color = MaterialTheme.colorScheme.error, ) } @@ -518,7 +564,7 @@ private fun CordnGroupInfo( text = { CordnExposureCard(it) }, confirmButton = { TextButton(onClick = { showExposure = false }) { - Text(stringRes(R.string.cordn_exposure_close)) + Text(stringRes(Res.string.cordn_exposure_close)) } }, ) @@ -577,15 +623,15 @@ private fun CordnAddMember( OutlinedTextField( value = search, onValueChange = onSearchChange, - label = { Text(stringRes(R.string.cordn_info_add_member)) }, - placeholder = { Text(stringRes(R.string.cordn_info_add_member_placeholder)) }, + label = { Text(stringRes(Res.string.cordn_info_add_member)) }, + placeholder = { Text(stringRes(Res.string.cordn_info_add_member_placeholder)) }, singleLine = true, enabled = !busy, modifier = Modifier.fillMaxWidth(), ) Text( - text = stringRes(R.string.cordn_info_add_member_note), + text = stringRes(Res.string.cordn_info_add_member_note), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = 4.dp), @@ -601,7 +647,7 @@ private fun CordnAddMember( modifier = SuggestionListDefaultHeightChat, onEmpty = { Text( - text = stringRes(R.string.cordn_info_add_member_none), + text = stringRes(Res.string.cordn_info_add_member_none), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), @@ -612,7 +658,7 @@ private fun CordnAddMember( if (reachable?.contains(user.pubkeyHex) == true) { Icon( symbol = MaterialSymbols.Key, - contentDescription = stringRes(R.string.cordn_info_has_key_package), + contentDescription = stringRes(Res.string.cordn_info_has_key_package), modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.primary, ) @@ -620,7 +666,7 @@ private fun CordnAddMember( IconButton(onClick = { onInvite(user) }) { Icon( symbol = MaterialSymbols.PersonAdd, - contentDescription = stringRes(R.string.cordn_info_add_member), + contentDescription = stringRes(Res.string.cordn_info_add_member), tint = MaterialTheme.colorScheme.primary, ) } @@ -643,20 +689,19 @@ private fun CordnAddMember( * better than silence — and the caller has logged the throwable in full either * way. */ -private fun adminFailureText( - context: Context, +private suspend fun adminFailureText( e: Throwable, who: String?, fallback: String, ): String { - val name = who ?: stringRes(context, R.string.cordn_admin_someone) + val name = who ?: loadStringRes(Res.string.cordn_admin_someone) return when (e) { is CordnGroupException -> when (e.reason) { - CordnGroupException.Reason.NO_KEY_PACKAGE -> stringRes(context, R.string.cordn_admin_no_key_package, name) - CordnGroupException.Reason.WRONG_KEY_PACKAGE_OWNER -> stringRes(context, R.string.cordn_admin_wrong_key_package, name) - CordnGroupException.Reason.NO_WELCOME -> stringRes(context, R.string.cordn_admin_no_welcome, name) - CordnGroupException.Reason.NOT_A_MEMBER -> stringRes(context, R.string.cordn_admin_not_a_member, name) + CordnGroupException.Reason.NO_KEY_PACKAGE -> loadStringRes(Res.string.cordn_admin_no_key_package, name) + CordnGroupException.Reason.WRONG_KEY_PACKAGE_OWNER -> loadStringRes(Res.string.cordn_admin_wrong_key_package, name) + CordnGroupException.Reason.NO_WELCOME -> loadStringRes(Res.string.cordn_admin_no_welcome, name) + CordnGroupException.Reason.NOT_A_MEMBER -> loadStringRes(Res.string.cordn_admin_not_a_member, name) CordnGroupException.Reason.OTHER -> e.message ?: fallback } @@ -664,14 +709,14 @@ private fun adminFailureText( // roster and the button disagreed rather than that anyone tried. is IllegalArgumentException -> if (e.message?.contains("self-removal") == true) { - stringRes(context, R.string.cordn_admin_no_self_remove) + loadStringRes(Res.string.cordn_admin_no_self_remove) } else { e.message ?: fallback } // A coordinator that does not answer is the single commonest failure // here and says nothing useful in its own words. - is TimeoutCancellationException -> stringRes(context, R.string.cordn_admin_coordinator_silent) + is TimeoutCancellationException -> loadStringRes(Res.string.cordn_admin_coordinator_silent) else -> e.message ?: fallback } @@ -701,7 +746,7 @@ private fun TechnicalDetails( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = stringRes(R.string.cordn_info_technical), + text = stringRes(Res.string.cordn_info_technical), style = MaterialTheme.typography.titleMedium, modifier = Modifier.weight(1f), ) @@ -725,22 +770,22 @@ private fun TechnicalDetails( val coordinator = remember(coordinatorPubKey) { LocalCache.getOrCreateUser(coordinatorPubKey) } CopyableKeyRow( - label = stringRes(R.string.cordn_info_coordinator_key), + label = stringRes(Res.string.cordn_info_coordinator_key), shown = coordinator.pubkeyDisplayHex(), copied = coordinator.pubkeyNpub(), copyDescription = stringRes(Res.string.copy_npub_to_clipboard), ) CopyableKeyRow( - label = stringRes(R.string.cordn_info_coordinator_nprofile), + label = stringRes(Res.string.cordn_info_coordinator_nprofile), shown = coordinator.toNProfile().toShortDisplay(6), copied = coordinator.toNProfile(), - copyDescription = stringRes(R.string.cordn_info_copy_nprofile), + copyDescription = stringRes(Res.string.cordn_info_copy_nprofile), ) SelectionContainer { Column { - InfoRow(stringRes(R.string.cordn_info_gid), gid) - InfoRow(stringRes(R.string.cordn_info_epoch), epoch.toString()) + InfoRow(stringRes(Res.string.cordn_info_gid), gid) + InfoRow(stringRes(Res.string.cordn_info_epoch), epoch.toString()) } } } @@ -835,7 +880,7 @@ private fun JoinRequests( ) { val runtime = accountViewModel.account.cordnRuntime ?: return val scope = rememberCoroutineScope() - val failed = stringRes(R.string.cordn_requests_failed) + val failed = stringRes(Res.string.cordn_requests_failed) var requests by remember { mutableStateOf<List<JoinRequest>?>(null) } var busy by remember { mutableStateOf(false) } @@ -853,15 +898,15 @@ private fun JoinRequests( } } - Text(stringRes(R.string.cordn_requests_title), style = MaterialTheme.typography.titleMedium) + Text(stringRes(Res.string.cordn_requests_title), style = MaterialTheme.typography.titleMedium) Text( - text = stringRes(R.string.cordn_requests_admin_only), + text = stringRes(Res.string.cordn_requests_admin_only), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) OutlinedButton(onClick = { scope.launch { reload() } }, enabled = !busy) { - Text(stringRes(R.string.cordn_requests_check)) + Text(stringRes(Res.string.cordn_requests_check)) } error?.let { Text(it, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.error) } @@ -870,7 +915,7 @@ private fun JoinRequests( if (loaded != null && !busy) { if (loaded.isEmpty()) { Text( - text = stringRes(R.string.cordn_requests_none), + text = stringRes(Res.string.cordn_requests_none), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -899,7 +944,7 @@ private fun JoinRequests( reload() } }) { - Text(stringRes(R.string.cordn_requests_accept)) + Text(stringRes(Res.string.cordn_requests_accept)) } TextButton(onClick = { scope.launch { @@ -911,7 +956,7 @@ private fun JoinRequests( reload() } }) { - Text(stringRes(R.string.cordn_requests_decline)) + Text(stringRes(Res.string.cordn_requests_decline)) } } } @@ -949,9 +994,9 @@ private fun ShareGroup( val clipboard = LocalClipboardManager.current var copied by remember { mutableStateOf(false) } - Text(stringRes(R.string.cordn_share_title), style = MaterialTheme.typography.titleMedium) + Text(stringRes(Res.string.cordn_share_title), style = MaterialTheme.typography.titleMedium) Text( - text = stringRes(R.string.cordn_share_explainer), + text = stringRes(Res.string.cordn_share_explainer), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -968,7 +1013,7 @@ private fun ShareGroup( }, modifier = Modifier.padding(top = 8.dp), ) { - Text(stringRes(if (copied) R.string.cordn_share_copied else R.string.cordn_share_copy)) + Text(stringRes(if (copied) Res.string.cordn_share_copied else Res.string.cordn_share_copy)) } } @@ -993,26 +1038,26 @@ private fun EditGroupDetailsDialog( AlertDialog( onDismissRequest = onDismiss, - title = { Text(stringRes(R.string.cordn_info_edit_details)) }, + title = { Text(stringRes(Res.string.cordn_info_edit_details)) }, text = { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { OutlinedTextField( value = draftName, onValueChange = { draftName = it }, - label = { Text(stringRes(R.string.cordn_create_name)) }, + label = { Text(stringRes(Res.string.cordn_create_name)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), ) OutlinedTextField( value = draftDescription, onValueChange = { draftDescription = it }, - label = { Text(stringRes(R.string.cordn_create_description)) }, + label = { Text(stringRes(Res.string.cordn_create_description)) }, singleLine = false, modifier = Modifier.fillMaxWidth(), ) if (admins.isNotEmpty()) { Text( - text = pluralStringRes(LocalContext.current, R.plurals.cordn_info_admins_kept, admins.size, admins.size), + text = pluralStringRes(Res.plurals.cordn_info_admins_kept, admins.size, admins.size), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -1024,7 +1069,7 @@ private fun EditGroupDetailsDialog( onClick = { onSave(draftName.trim(), draftDescription.trim()) }, enabled = draftName.isNotBlank(), ) { - Text(stringRes(R.string.cordn_info_save)) + Text(stringRes(Res.string.cordn_info_save)) } }, dismissButton = { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupListScreen.kt index e7a5defb33..13876ffbe1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupListScreen.kt @@ -41,10 +41,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.cordn.CoordinatorHealth import com.vitorpamplona.amethyst.commons.cordn.ui.CoordinatorHealthRow import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -52,6 +50,14 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.cordnGroups.CordnGroupChatroom import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable +import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable_detail +import com.vitorpamplona.amethyst.commons.resources.cordn_groups_count +import com.vitorpamplona.amethyst.commons.resources.cordn_groups_down_note +import com.vitorpamplona.amethyst.commons.resources.cordn_groups_manage +import com.vitorpamplona.amethyst.commons.resources.cordn_groups_none +import com.vitorpamplona.amethyst.commons.resources.cordn_groups_none_detail +import com.vitorpamplona.amethyst.commons.resources.cordn_groups_start import com.vitorpamplona.amethyst.commons.resources.cordn_groups_title import com.vitorpamplona.amethyst.commons.ui.components.EmptyState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -106,14 +112,14 @@ fun CordnGroupListScreen( IconButton(onClick = { nav.nav(Route.CordnCoordinators) }) { Icon( symbol = MaterialSymbols.Dns, - contentDescription = stringRes(R.string.cordn_groups_manage), + contentDescription = stringRes(Res.string.cordn_groups_manage), modifier = Modifier.size(22.dp), ) } IconButton(onClick = { nav.nav(Route.CordnCreateGroup) }) { Icon( symbol = MaterialSymbols.Add, - contentDescription = stringRes(R.string.cordn_groups_start), + contentDescription = stringRes(Res.string.cordn_groups_start), modifier = Modifier.size(24.dp), ) } @@ -122,8 +128,8 @@ fun CordnGroupListScreen( ) { padding -> if (runtime == null) { EmptyState( - title = stringRes(R.string.cordn_group_unavailable), - description = stringRes(R.string.cordn_group_unavailable_detail), + title = stringRes(Res.string.cordn_group_unavailable), + description = stringRes(Res.string.cordn_group_unavailable_detail), modifier = Modifier.padding(padding), ) return@Scaffold @@ -148,8 +154,8 @@ fun CordnGroupListScreen( if (sections.isEmpty()) { EmptyState( - title = stringRes(R.string.cordn_groups_none), - description = stringRes(R.string.cordn_groups_none_detail), + title = stringRes(Res.string.cordn_groups_none), + description = stringRes(Res.string.cordn_groups_none_detail), modifier = Modifier.padding(padding), ) return@Scaffold @@ -230,7 +236,7 @@ private fun CoordinatorSection( }, ) Text( - text = pluralStringRes(LocalContext.current, R.plurals.cordn_groups_count, groupCount, groupCount), + text = pluralStringRes(Res.plurals.cordn_groups_count, groupCount, groupCount), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -248,7 +254,7 @@ private fun CoordinatorSection( verticalAlignment = Alignment.Top, ) { Text( - text = stringRes(R.string.cordn_groups_down_note), + text = stringRes(Res.string.cordn_groups_down_note), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.error, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnInvitationsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnInvitationsScreen.kt index a1292ad0e6..b145042c85 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnInvitationsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnInvitationsScreen.kt @@ -48,16 +48,30 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.back +import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable +import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable_detail import com.vitorpamplona.amethyst.commons.resources.cordn_group_untitled +import com.vitorpamplona.amethyst.commons.resources.cordn_invitations_accept +import com.vitorpamplona.amethyst.commons.resources.cordn_invitations_decline +import com.vitorpamplona.amethyst.commons.resources.cordn_invitations_decline_warning +import com.vitorpamplona.amethyst.commons.resources.cordn_invitations_explainer +import com.vitorpamplona.amethyst.commons.resources.cordn_invitations_load_failed +import com.vitorpamplona.amethyst.commons.resources.cordn_invitations_members +import com.vitorpamplona.amethyst.commons.resources.cordn_invitations_members_more +import com.vitorpamplona.amethyst.commons.resources.cordn_invitations_no_coordinators +import com.vitorpamplona.amethyst.commons.resources.cordn_invitations_none +import com.vitorpamplona.amethyst.commons.resources.cordn_invitations_refresh +import com.vitorpamplona.amethyst.commons.resources.cordn_invitations_skipped +import com.vitorpamplona.amethyst.commons.resources.cordn_invitations_title +import com.vitorpamplona.amethyst.commons.resources.cordn_invitations_unreachable +import com.vitorpamplona.amethyst.commons.resources.cordn_invitations_via import com.vitorpamplona.amethyst.commons.ui.components.EmptyState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.model.cordn.CordnInvitation @@ -69,6 +83,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.CoordinatorI import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.coordinatorDisplayName import com.vitorpamplona.amethyst.ui.stringRes import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.pluralStringResource /** * Invitations to cordn groups, waiting to be answered. @@ -106,7 +121,7 @@ fun CordnInvitationsScreen( var invitations by remember { mutableStateOf<CordnInvitations?>(null) } var busy by remember { mutableStateOf(false) } var error by remember { mutableStateOf<String?>(null) } - val loadFailed = stringRes(R.string.cordn_invitations_load_failed) + val loadFailed = stringRes(Res.string.cordn_invitations_load_failed) suspend fun reload() { busy = true @@ -130,10 +145,10 @@ fun CordnInvitationsScreen( Icon(MaterialSymbols.AutoMirrored.ArrowBack, contentDescription = stringRes(Res.string.back)) } }, - title = { Text(stringRes(R.string.cordn_invitations_title)) }, + title = { Text(stringRes(Res.string.cordn_invitations_title)) }, actions = { IconButton(onClick = { scope.launch { reload() } }, enabled = !busy) { - Icon(MaterialSymbols.Refresh, contentDescription = stringRes(R.string.cordn_invitations_refresh)) + Icon(MaterialSymbols.Refresh, contentDescription = stringRes(Res.string.cordn_invitations_refresh)) } }, ) @@ -150,8 +165,8 @@ fun CordnInvitationsScreen( ) { if (runtime == null) { EmptyState( - title = stringRes(R.string.cordn_group_unavailable), - description = stringRes(R.string.cordn_group_unavailable_detail), + title = stringRes(Res.string.cordn_group_unavailable), + description = stringRes(Res.string.cordn_group_unavailable_detail), ) return@Column } @@ -163,7 +178,7 @@ fun CordnInvitationsScreen( val coordinators by runtime.coordinators.collectAsStateWithLifecycle() Text( - text = stringRes(R.string.cordn_invitations_explainer), + text = stringRes(Res.string.cordn_invitations_explainer), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -212,7 +227,7 @@ fun CordnInvitationsScreen( NoticeCard( title = stringRes( - R.string.cordn_invitations_unreachable, + Res.string.cordn_invitations_unreachable, coordinatorDisplayName(it.coordinator.pubKey, it.coordinator.label, accountViewModel), ), detail = it.reason, @@ -225,7 +240,7 @@ fun CordnInvitationsScreen( // friend swears they sent simply does not exist here. loaded.skipped.forEach { NoticeCard( - title = stringRes(R.string.cordn_invitations_skipped), + title = stringRes(Res.string.cordn_invitations_skipped), detail = it.reason, isError = false, ) @@ -235,9 +250,9 @@ fun CordnInvitationsScreen( Text( text = if (coordinators.isEmpty()) { - stringRes(R.string.cordn_invitations_no_coordinators) + stringRes(Res.string.cordn_invitations_no_coordinators) } else { - stringRes(R.string.cordn_invitations_none) + stringRes(Res.string.cordn_invitations_none) }, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -276,7 +291,7 @@ private fun InvitationCard( // "Already in it", never "invited you" — see the screen KDoc. Text( - text = pluralStringResource(R.plurals.cordn_invitations_members, welcome.members.size, welcome.members.size), + text = pluralStringResource(Res.plurals.cordn_invitations_members, welcome.members.size, welcome.members.size), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -300,7 +315,7 @@ private fun InvitationCard( } if (welcome.members.size > MEMBER_FACES) { Text( - text = stringRes(R.string.cordn_invitations_members_more, welcome.members.size - MEMBER_FACES), + text = stringRes(Res.string.cordn_invitations_members_more, welcome.members.size - MEMBER_FACES), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -329,7 +344,7 @@ private fun InvitationCard( size = 20.dp, ) { name -> Text( - text = stringRes(R.string.cordn_invitations_via, name), + text = stringRes(Res.string.cordn_invitations_via, name), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -340,11 +355,11 @@ private fun InvitationCard( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { - Button(onClick = onAccept) { Text(stringRes(R.string.cordn_invitations_accept)) } - TextButton(onClick = onDecline) { Text(stringRes(R.string.cordn_invitations_decline)) } + Button(onClick = onAccept) { Text(stringRes(Res.string.cordn_invitations_accept)) } + TextButton(onClick = onDecline) { Text(stringRes(Res.string.cordn_invitations_decline)) } } Text( - text = stringRes(R.string.cordn_invitations_decline_warning), + text = stringRes(Res.string.cordn_invitations_decline_warning), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt index 189fca42a9..031512faea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt @@ -55,7 +55,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.chats.ui.ChatDivisor import com.vitorpamplona.amethyst.commons.chats.ui.UserDisplayNameLayout import com.vitorpamplona.amethyst.commons.cordn.CordnMentions @@ -67,6 +66,24 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.copied_to_clipboard import com.vitorpamplona.amethyst.commons.resources.copy_text +import com.vitorpamplona.amethyst.commons.resources.cordn_action_delete +import com.vitorpamplona.amethyst.commons.resources.cordn_action_edit +import com.vitorpamplona.amethyst.commons.resources.cordn_action_go_to_message +import com.vitorpamplona.amethyst.commons.resources.cordn_action_pin +import com.vitorpamplona.amethyst.commons.resources.cordn_action_reply +import com.vitorpamplona.amethyst.commons.resources.cordn_action_unpin +import com.vitorpamplona.amethyst.commons.resources.cordn_chat_unread_divider +import com.vitorpamplona.amethyst.commons.resources.cordn_chat_yesterday +import com.vitorpamplona.amethyst.commons.resources.cordn_delete_confirm_body +import com.vitorpamplona.amethyst.commons.resources.cordn_delete_confirm_title +import com.vitorpamplona.amethyst.commons.resources.cordn_delivery_accepted +import com.vitorpamplona.amethyst.commons.resources.cordn_message_deleted +import com.vitorpamplona.amethyst.commons.resources.cordn_message_details_coordinator +import com.vitorpamplona.amethyst.commons.resources.cordn_message_details_cursor +import com.vitorpamplona.amethyst.commons.resources.cordn_message_details_sent_at +import com.vitorpamplona.amethyst.commons.resources.cordn_message_details_title +import com.vitorpamplona.amethyst.commons.resources.cordn_message_edited +import com.vitorpamplona.amethyst.commons.resources.cordn_reactions_title import com.vitorpamplona.amethyst.commons.resources.quick_action_share import com.vitorpamplona.amethyst.commons.resources.today import com.vitorpamplona.amethyst.commons.ui.components.ClickableBox @@ -292,7 +309,7 @@ private fun CordnBubbleContents( when { text == null -> Text( - text = stringRes(R.string.cordn_message_deleted), + text = stringRes(Res.string.cordn_message_deleted), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -448,7 +465,7 @@ private fun CordnReactionDetailSheet( ) { Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(bottom = 24.dp)) { Text( - text = stringRes(R.string.cordn_reactions_title), + text = stringRes(Res.string.cordn_reactions_title), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(horizontal = 20.dp, vertical = 12.dp), ) @@ -519,7 +536,7 @@ internal fun CordnQuotedMessage( val body = if (annotations.isDeleted(parent.envelope.id)) { - stringRes(R.string.cordn_message_deleted) + stringRes(Res.string.cordn_message_deleted) } else { annotations.contentOf(parent.envelope.id).orEmpty() } @@ -575,7 +592,7 @@ private fun CordnQuoteActionSheet( Column(Modifier.padding(bottom = 24.dp)) { TileRow { if (onGoToMessage != null) { - ActionTile(MaterialSymbols.ArrowUpward, stringRes(R.string.cordn_action_go_to_message)) { + ActionTile(MaterialSymbols.ArrowUpward, stringRes(Res.string.cordn_action_go_to_message)) { onGoToMessage() onDismiss() } @@ -594,7 +611,7 @@ private fun CordnQuoteActionSheet( */ @Composable internal fun UnreadDivider() { - ChatDivisor(stringRes(R.string.cordn_chat_unread_divider), MaterialTheme.colorScheme.primary) + ChatDivisor(stringRes(Res.string.cordn_chat_unread_divider), MaterialTheme.colorScheme.primary) } /** @@ -618,7 +635,7 @@ private fun CordnMessageFooter( if (isPinned) { Icon( symbol = MaterialSymbols.PushPin, - contentDescription = stringRes(R.string.cordn_action_pin), + contentDescription = stringRes(Res.string.cordn_action_pin), modifier = Modifier.size(12.dp), tint = MaterialTheme.colorScheme.primary, ) @@ -627,7 +644,7 @@ private fun CordnMessageFooter( if (isEdited) { Text( - text = stringRes(R.string.cordn_message_edited), + text = stringRes(Res.string.cordn_message_edited), fontSize = Font12SP, color = MaterialTheme.colorScheme.placeholderText, maxLines = 1, @@ -658,7 +675,7 @@ private fun CordnMessageFooter( Spacer(StdHorzSpacer) Icon( symbol = MaterialSymbols.Done, - contentDescription = stringRes(R.string.cordn_delivery_accepted), + contentDescription = stringRes(Res.string.cordn_delivery_accepted), modifier = Modifier.size(12.dp), tint = MaterialTheme.colorScheme.allGoodColor, ) @@ -711,18 +728,18 @@ private fun CordnMessageDetailsSheet( verticalArrangement = Arrangement.spacedBy(6.dp), ) { Text( - text = stringRes(R.string.cordn_message_details_title), + text = stringRes(Res.string.cordn_message_details_title), style = MaterialTheme.typography.titleSmall, ) - DetailLine(stringRes(R.string.cordn_message_details_sent_at), sentAt) - DetailLine(stringRes(R.string.cordn_message_details_cursor), message.cursor.toString()) + DetailLine(stringRes(Res.string.cordn_message_details_sent_at), sentAt) + DetailLine(stringRes(Res.string.cordn_message_details_cursor), message.cursor.toString()) DetailLine( - stringRes(R.string.cordn_message_details_coordinator), + stringRes(Res.string.cordn_message_details_coordinator), coordinatorDisplayName(coordinatorPubKey, label = null, accountViewModel = accountViewModel), ) if (isMine) { Text( - text = stringRes(R.string.cordn_delivery_accepted), + text = stringRes(Res.string.cordn_delivery_accepted), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.allGoodColor, ) @@ -797,10 +814,10 @@ private fun CordnMessageActionSheet( if (confirmingDelete) { QuickActionAlertDialog( - title = stringRes(R.string.cordn_delete_confirm_title), - textContent = stringRes(R.string.cordn_delete_confirm_body), + title = stringRes(Res.string.cordn_delete_confirm_title), + textContent = stringRes(Res.string.cordn_delete_confirm_body), buttonIcon = MaterialSymbols.Delete, - buttonText = stringRes(R.string.cordn_action_delete), + buttonText = stringRes(Res.string.cordn_action_delete), onClickDoOnce = performDelete, onClickDontShowAgain = { accountViewModel.account.settings.setHideDeleteRequestDialog() @@ -843,7 +860,7 @@ private fun CordnMessageActionSheet( TileRow { if (isLive) { - ActionTile(MaterialSymbols.AutoMirrored.Chat, stringRes(R.string.cordn_action_reply)) { + ActionTile(MaterialSymbols.AutoMirrored.Chat, stringRes(Res.string.cordn_action_reply)) { onReply() onDismiss() } @@ -853,7 +870,7 @@ private fun CordnMessageActionSheet( // message rather than only on your own. ActionTile( MaterialSymbols.PushPin, - stringRes(if (isPinned) R.string.cordn_action_unpin else R.string.cordn_action_pin), + stringRes(if (isPinned) Res.string.cordn_action_unpin else Res.string.cordn_action_pin), ) { onTogglePin() onDismiss() @@ -863,11 +880,11 @@ private fun CordnMessageActionSheet( // anyone else. Hiding them here is the same rule, stated where it stops // being a surprise. if (isMine && isLive) { - ActionTile(MaterialSymbols.Edit, stringRes(R.string.cordn_action_edit)) { + ActionTile(MaterialSymbols.Edit, stringRes(Res.string.cordn_action_edit)) { onEdit() onDismiss() } - ActionTile(MaterialSymbols.Delete, stringRes(R.string.cordn_action_delete), isDestructive = true) { + ActionTile(MaterialSymbols.Delete, stringRes(Res.string.cordn_action_delete), isDestructive = true) { // A withdrawal cannot be taken back and the coordinator keeps // the ciphertext either way, so it is worth one question — // unless the account has already opted out of being asked. @@ -989,7 +1006,7 @@ internal fun DaySeparator( val label = when (day) { today -> stringRes(Res.string.today) - today.minusDays(1) -> stringRes(R.string.cordn_chat_yesterday) + today.minusDays(1) -> stringRes(Res.string.cordn_chat_yesterday) // Year included only when it is not this one: printing 2026 on every // divider all year is noise. else -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnBackupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnBackupScreen.kt index 3560b60be6..0876d4cf5d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnBackupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnBackupScreen.kt @@ -52,13 +52,26 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.cancel +import com.vitorpamplona.amethyst.commons.resources.cordn_backup_contents_body +import com.vitorpamplona.amethyst.commons.resources.cordn_backup_contents_title +import com.vitorpamplona.amethyst.commons.resources.cordn_backup_explainer +import com.vitorpamplona.amethyst.commons.resources.cordn_backup_export +import com.vitorpamplona.amethyst.commons.resources.cordn_backup_exported +import com.vitorpamplona.amethyst.commons.resources.cordn_backup_failed +import com.vitorpamplona.amethyst.commons.resources.cordn_backup_passphrase +import com.vitorpamplona.amethyst.commons.resources.cordn_backup_restore +import com.vitorpamplona.amethyst.commons.resources.cordn_backup_restore_body +import com.vitorpamplona.amethyst.commons.resources.cordn_backup_restore_confirm_body +import com.vitorpamplona.amethyst.commons.resources.cordn_backup_restore_confirm_title +import com.vitorpamplona.amethyst.commons.resources.cordn_backup_restored import com.vitorpamplona.amethyst.commons.resources.cordn_backup_section_export import com.vitorpamplona.amethyst.commons.resources.cordn_backup_section_passphrase import com.vitorpamplona.amethyst.commons.resources.cordn_backup_section_restore import com.vitorpamplona.amethyst.commons.resources.cordn_backup_title +import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable +import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable_detail import com.vitorpamplona.amethyst.commons.ui.components.EmptyState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton @@ -113,9 +126,9 @@ fun CordnBackupScreen( var error by remember { mutableStateOf<String?>(null) } var pendingRestore by remember { mutableStateOf<Uri?>(null) } - val exported = stringRes(R.string.cordn_backup_exported) - val restored = stringRes(R.string.cordn_backup_restored) - val failed = stringRes(R.string.cordn_backup_failed) + val exported = stringRes(Res.string.cordn_backup_exported) + val restored = stringRes(Res.string.cordn_backup_restored) + val failed = stringRes(Res.string.cordn_backup_failed) val saver = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("application/octet-stream")) { uri -> @@ -179,8 +192,8 @@ fun CordnBackupScreen( // The app's own empty state, centred and titled, rather than a // sentence stranded in the top-left corner. EmptyState( - title = stringRes(R.string.cordn_group_unavailable), - description = stringRes(R.string.cordn_group_unavailable_detail), + title = stringRes(Res.string.cordn_group_unavailable), + description = stringRes(Res.string.cordn_group_unavailable_detail), modifier = Modifier.padding(padding), ) return@Scaffold @@ -196,7 +209,7 @@ fun CordnBackupScreen( verticalArrangement = Arrangement.spacedBy(20.dp), ) { Text( - text = stringRes(R.string.cordn_backup_explainer), + text = stringRes(Res.string.cordn_backup_explainer), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -212,7 +225,7 @@ fun CordnBackupScreen( passphrase = it error = null }, - label = { Text(stringRes(R.string.cordn_backup_passphrase)) }, + label = { Text(stringRes(Res.string.cordn_backup_passphrase)) }, visualTransformation = PasswordVisualTransformation(), // The dots are only half of it. Without the password // keyboard type the IME treats this as ordinary prose: @@ -230,9 +243,9 @@ fun CordnBackupScreen( SettingsSection(Res.string.cordn_backup_section_export) { SettingsFormBlock { - Text(stringRes(R.string.cordn_backup_contents_title), style = MaterialTheme.typography.titleSmall) + Text(stringRes(Res.string.cordn_backup_contents_title), style = MaterialTheme.typography.titleSmall) Text( - text = stringRes(R.string.cordn_backup_contents_body), + text = stringRes(Res.string.cordn_backup_contents_body), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -248,7 +261,7 @@ fun CordnBackupScreen( // Key derivation here is scrypt, which is slow on // purpose, so a greyed-out button was the only sign // anything was happening for several seconds. - BusyLabel(busy, stringRes(R.string.cordn_backup_export)) + BusyLabel(busy, stringRes(Res.string.cordn_backup_export)) } } } @@ -256,7 +269,7 @@ fun CordnBackupScreen( SettingsSection(Res.string.cordn_backup_section_restore) { SettingsFormBlock { Text( - text = stringRes(R.string.cordn_backup_restore_body), + text = stringRes(Res.string.cordn_backup_restore_body), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -266,7 +279,7 @@ fun CordnBackupScreen( enabled = !busy && passphrase.isNotBlank(), modifier = Modifier.fillMaxWidth(), ) { - BusyLabel(busy, stringRes(R.string.cordn_backup_restore)) + BusyLabel(busy, stringRes(Res.string.cordn_backup_restore)) } } } @@ -276,8 +289,8 @@ fun CordnBackupScreen( pendingRestore?.let { uri -> AlertDialog( onDismissRequest = { pendingRestore = null }, - title = { Text(stringRes(R.string.cordn_backup_restore_confirm_title)) }, - text = { Text(stringRes(R.string.cordn_backup_restore_confirm_body)) }, + title = { Text(stringRes(Res.string.cordn_backup_restore_confirm_title)) }, + text = { Text(stringRes(Res.string.cordn_backup_restore_confirm_body)) }, confirmButton = { TextButton(onClick = { pendingRestore = null @@ -302,7 +315,7 @@ fun CordnBackupScreen( } } }) { - Text(stringRes(R.string.cordn_backup_restore), color = MaterialTheme.colorScheme.error) + Text(stringRes(Res.string.cordn_backup_restore), color = MaterialTheme.colorScheme.error) } }, dismissButton = { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt index 01b0a3c93f..3459eec409 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt @@ -58,7 +58,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.cordn.CoordinatorConfig import com.vitorpamplona.amethyst.commons.cordn.CoordinatorHealth import com.vitorpamplona.amethyst.commons.cordn.CordnCoordinatorDiscovery @@ -68,10 +67,45 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.cancel +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_add_action +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_add_failed +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_copy_key +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_discover_action +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_discover_add +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_discover_explainer +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_discover_failed +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_discover_none +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_discover_seen +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_discover_unheard +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_explainer +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_health_down +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_health_ok +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_health_retrying +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_health_unknown +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_identify +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_key +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_label +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_label_note +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_more +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_none +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_purge +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_purge_body +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_purge_title +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_relays_label +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_remove +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_rename import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_section_discover import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_section_manual import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_section_yours +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_server +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_server_claim +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_server_silent import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_title +import com.vitorpamplona.amethyst.commons.resources.cordn_create_coordinator_pubkey +import com.vitorpamplona.amethyst.commons.resources.cordn_create_coordinator_relays +import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable +import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable_detail +import com.vitorpamplona.amethyst.commons.resources.cordn_info_save import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled import com.vitorpamplona.amethyst.commons.ui.components.EmptyState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -127,8 +161,8 @@ fun CordnCoordinatorsScreen( // The app's own empty state, centred and titled, rather than a // sentence stranded in the top-left corner. EmptyState( - title = stringRes(R.string.cordn_group_unavailable), - description = stringRes(R.string.cordn_group_unavailable_detail), + title = stringRes(Res.string.cordn_group_unavailable), + description = stringRes(Res.string.cordn_group_unavailable_detail), modifier = Modifier.padding(padding), ) return@Scaffold @@ -146,7 +180,7 @@ fun CordnCoordinatorsScreen( verticalArrangement = Arrangement.spacedBy(20.dp), ) { Text( - text = stringRes(R.string.cordn_coordinators_explainer), + text = stringRes(Res.string.cordn_coordinators_explainer), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -165,7 +199,7 @@ fun CordnCoordinatorsScreen( if (coordinators.isEmpty()) { Text( - text = stringRes(R.string.cordn_coordinators_none), + text = stringRes(Res.string.cordn_coordinators_none), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -225,7 +259,7 @@ private fun CoordinatorCard( IconButton(onClick = { renaming = true }) { Icon( symbol = MaterialSymbols.Edit, - contentDescription = stringRes(R.string.cordn_coordinators_rename), + contentDescription = stringRes(Res.string.cordn_coordinators_rename), modifier = Modifier.size(18.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -264,10 +298,10 @@ private fun CoordinatorCard( // away on the clipboard, which is how a comparison is actually done. val coordinator = remember(config.pubKey) { LocalCache.getOrCreateUser(config.pubKey) } CopyableKeyRow( - label = stringRes(R.string.cordn_coordinators_key), + label = stringRes(Res.string.cordn_coordinators_key), shown = coordinator.pubkeyDisplayHex(), copied = coordinator.pubkeyNpub(), - copyDescription = stringRes(R.string.cordn_coordinators_copy_key), + copyDescription = stringRes(Res.string.cordn_coordinators_copy_key), ) // Every relay, not the first two and a count: this is the management @@ -275,7 +309,7 @@ private fun CoordinatorCard( // came here to check or to rule out. Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { Text( - text = stringRes(R.string.cordn_coordinators_relays_label), + text = stringRes(Res.string.cordn_coordinators_relays_label), style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.SemiBold, ) @@ -304,10 +338,10 @@ private fun CoordinatorCard( text = info?.let { stringRes( - R.string.cordn_coordinators_server, + Res.string.cordn_coordinators_server, listOfNotNull(it.name, it.version, it.protocolVersion).joinToString(" \u00b7 "), ) - } ?: stringRes(R.string.cordn_coordinators_server_silent), + } ?: stringRes(Res.string.cordn_coordinators_server_silent), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -315,7 +349,7 @@ private fun CoordinatorCard( // Said next to it, every time, because a name on a settings // screen reads as verified and this one is not: §8.5 makes // the pubkey the identity and nothing else. - text = stringRes(R.string.cordn_coordinators_server_claim), + text = stringRes(Res.string.cordn_coordinators_server_claim), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -333,7 +367,7 @@ private fun CoordinatorCard( infoChecked = true } }) { - Text(stringRes(R.string.cordn_coordinators_identify)) + Text(stringRes(Res.string.cordn_coordinators_identify)) } Spacer(Modifier.weight(1f)) @@ -345,14 +379,14 @@ private fun CoordinatorCard( IconButton(onClick = { menuOpen = true }) { Icon( symbol = MaterialSymbols.MoreVert, - contentDescription = stringRes(R.string.cordn_coordinators_more), + contentDescription = stringRes(Res.string.cordn_coordinators_more), modifier = Modifier.size(20.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) { DropdownMenuItem( - text = { Text(stringRes(R.string.cordn_coordinators_remove)) }, + text = { Text(stringRes(Res.string.cordn_coordinators_remove)) }, onClick = { menuOpen = false scope.launch { runtime.forget(config.pubKey) } @@ -361,7 +395,7 @@ private fun CoordinatorCard( DropdownMenuItem( text = { Text( - text = stringRes(R.string.cordn_coordinators_purge), + text = stringRes(Res.string.cordn_coordinators_purge), color = MaterialTheme.colorScheme.error, ) }, @@ -378,14 +412,14 @@ private fun CoordinatorCard( if (confirmingPurge) { AlertDialog( onDismissRequest = { confirmingPurge = false }, - title = { Text(stringRes(R.string.cordn_coordinators_purge_title)) }, - text = { Text(stringRes(R.string.cordn_coordinators_purge_body)) }, + title = { Text(stringRes(Res.string.cordn_coordinators_purge_title)) }, + text = { Text(stringRes(Res.string.cordn_coordinators_purge_body)) }, confirmButton = { TextButton(onClick = { confirmingPurge = false scope.launch { runtime.purge(config.pubKey) } }) { - Text(stringRes(R.string.cordn_coordinators_purge), color = MaterialTheme.colorScheme.error) + Text(stringRes(Res.string.cordn_coordinators_purge), color = MaterialTheme.colorScheme.error) } }, dismissButton = { @@ -409,10 +443,10 @@ private fun CoordinatorCard( private fun HealthLine(state: CoordinatorHealth.State) { val text = when { - state.isUnknown -> stringRes(R.string.cordn_coordinators_health_unknown) - state.isDown -> stringRes(R.string.cordn_coordinators_health_down, state.consecutiveFailures) - state.consecutiveFailures > 0 -> stringRes(R.string.cordn_coordinators_health_retrying) - else -> stringRes(R.string.cordn_coordinators_health_ok) + state.isUnknown -> stringRes(Res.string.cordn_coordinators_health_unknown) + state.isDown -> stringRes(Res.string.cordn_coordinators_health_down, state.consecutiveFailures) + state.consecutiveFailures > 0 -> stringRes(Res.string.cordn_coordinators_health_retrying) + else -> stringRes(Res.string.cordn_coordinators_health_ok) } Text( @@ -447,10 +481,10 @@ private fun DiscoverCoordinators( var result by remember { mutableStateOf<CordnCoordinatorDiscovery.Result?>(null) } var busy by remember { mutableStateOf(false) } var error by remember { mutableStateOf<String?>(null) } - val failed = stringRes(R.string.cordn_coordinators_discover_failed) + val failed = stringRes(Res.string.cordn_coordinators_discover_failed) Text( - text = stringRes(R.string.cordn_coordinators_discover_explainer), + text = stringRes(Res.string.cordn_coordinators_discover_explainer), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -475,7 +509,7 @@ private fun DiscoverCoordinators( CircularProgressIndicator(Modifier.size(16.dp), strokeWidth = 2.dp) Spacer(Modifier.width(8.dp)) } - Text(stringRes(R.string.cordn_coordinators_discover_action)) + Text(stringRes(Res.string.cordn_coordinators_discover_action)) } error?.let { Text(it, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.error) } @@ -497,11 +531,11 @@ private fun DiscoverCoordinators( Text( text = if (found.unreachable.isEmpty()) { - stringRes(R.string.cordn_coordinators_discover_none) + stringRes(Res.string.cordn_coordinators_discover_none) } else { // "Nobody is announcing" and "we were not told" are // different answers and only one of them is final. - stringRes(R.string.cordn_coordinators_discover_unheard, found.unreachable.size) + stringRes(Res.string.cordn_coordinators_discover_unheard, found.unreachable.size) }, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -529,19 +563,19 @@ private fun RenameCoordinatorDialog( AlertDialog( onDismissRequest = onDismiss, - title = { Text(stringRes(R.string.cordn_coordinators_rename)) }, + title = { Text(stringRes(Res.string.cordn_coordinators_rename)) }, text = { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { OutlinedTextField( value = input, onValueChange = { input = it }, - label = { Text(stringRes(R.string.cordn_coordinators_label)) }, + label = { Text(stringRes(Res.string.cordn_coordinators_label)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), ) Text( // The same caution the add form gives, for the same reason. - text = stringRes(R.string.cordn_coordinators_label_note), + text = stringRes(Res.string.cordn_coordinators_label_note), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -549,7 +583,7 @@ private fun RenameCoordinatorDialog( }, confirmButton = { TextButton(onClick = { onSave(input.trim().ifEmpty { null }) }) { - Text(stringRes(R.string.cordn_info_save)) + Text(stringRes(Res.string.cordn_info_save)) } }, dismissButton = { @@ -601,7 +635,7 @@ private fun DiscoveredCard( Text( // Staleness matters more here than anywhere: the last live // survey found most announcements were months-dead demos. - text = stringRes(R.string.cordn_coordinators_discover_seen, timeAgoNoDot(offer.announcedAt).trim()), + text = stringRes(Res.string.cordn_coordinators_discover_seen, timeAgoNoDot(offer.announcedAt).trim()), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -619,7 +653,7 @@ private fun DiscoveredCard( enabled = !busy, modifier = Modifier.padding(top = 4.dp), ) { - Text(stringRes(R.string.cordn_coordinators_discover_add)) + Text(stringRes(Res.string.cordn_coordinators_discover_add)) } } } @@ -633,7 +667,7 @@ private fun AddCoordinator(runtime: CordnRuntime) { var labelInput by remember { mutableStateOf("") } var error by remember { mutableStateOf<String?>(null) } var busy by remember { mutableStateOf(false) } - val failed = stringRes(R.string.cordn_coordinators_add_failed) + val failed = stringRes(Res.string.cordn_coordinators_add_failed) val config = remember(pubKeyInput, relaysInput, labelInput) { @@ -652,7 +686,7 @@ private fun AddCoordinator(runtime: CordnRuntime) { pubKeyInput = it error = null }, - label = { Text(stringRes(R.string.cordn_create_coordinator_pubkey)) }, + label = { Text(stringRes(Res.string.cordn_create_coordinator_pubkey)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), ) @@ -662,14 +696,14 @@ private fun AddCoordinator(runtime: CordnRuntime) { relaysInput = it error = null }, - label = { Text(stringRes(R.string.cordn_create_coordinator_relays)) }, + label = { Text(stringRes(Res.string.cordn_create_coordinator_relays)) }, singleLine = false, modifier = Modifier.fillMaxWidth(), ) OutlinedTextField( value = labelInput, onValueChange = { labelInput = it }, - label = { Text(stringRes(R.string.cordn_coordinators_label)) }, + label = { Text(stringRes(Res.string.cordn_coordinators_label)) }, singleLine = true, modifier = Modifier.fillMaxWidth(), ) @@ -677,7 +711,7 @@ private fun AddCoordinator(runtime: CordnRuntime) { Text( // A label is ours, never theirs. A coordinator cannot prove a name and // this one is not asked for one. - text = stringRes(R.string.cordn_coordinators_label_note), + text = stringRes(Res.string.cordn_coordinators_label_note), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -705,6 +739,6 @@ private fun AddCoordinator(runtime: CordnRuntime) { enabled = !busy && config != null, modifier = Modifier.fillMaxWidth(), ) { - Text(stringRes(R.string.cordn_coordinators_add_action)) + Text(stringRes(Res.string.cordn_coordinators_add_action)) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnKeyPackagesScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnKeyPackagesScreen.kt index 5f33e13b24..b3ebf82863 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnKeyPackagesScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnKeyPackagesScreen.kt @@ -47,14 +47,28 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.cordn.CoordinatorConfig import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_none +import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable +import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable_detail +import com.vitorpamplona.amethyst.commons.resources.cordn_keypackages_disclosure +import com.vitorpamplona.amethyst.commons.resources.cordn_keypackages_explainer +import com.vitorpamplona.amethyst.commons.resources.cordn_keypackages_failed +import com.vitorpamplona.amethyst.commons.resources.cordn_keypackages_last_resort_no +import com.vitorpamplona.amethyst.commons.resources.cordn_keypackages_last_resort_yes +import com.vitorpamplona.amethyst.commons.resources.cordn_keypackages_orphans +import com.vitorpamplona.amethyst.commons.resources.cordn_keypackages_orphans_body +import com.vitorpamplona.amethyst.commons.resources.cordn_keypackages_publish +import com.vitorpamplona.amethyst.commons.resources.cordn_keypackages_publish_last_resort import com.vitorpamplona.amethyst.commons.resources.cordn_keypackages_section +import com.vitorpamplona.amethyst.commons.resources.cordn_keypackages_summary import com.vitorpamplona.amethyst.commons.resources.cordn_keypackages_title +import com.vitorpamplona.amethyst.commons.resources.cordn_keypackages_topup_note +import com.vitorpamplona.amethyst.commons.resources.cordn_keypackages_withdraw_all +import com.vitorpamplona.amethyst.commons.resources.cordn_keypackages_withdraw_orphans import com.vitorpamplona.amethyst.commons.ui.components.EmptyState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton @@ -106,8 +120,8 @@ fun CordnKeyPackagesScreen( ) { padding -> if (runtime == null) { EmptyState( - title = stringRes(R.string.cordn_group_unavailable), - description = stringRes(R.string.cordn_group_unavailable_detail), + title = stringRes(Res.string.cordn_group_unavailable), + description = stringRes(Res.string.cordn_group_unavailable_detail), modifier = Modifier.padding(padding), ) return@Scaffold @@ -125,7 +139,7 @@ fun CordnKeyPackagesScreen( verticalArrangement = Arrangement.spacedBy(20.dp), ) { Text( - text = stringRes(R.string.cordn_keypackages_explainer), + text = stringRes(Res.string.cordn_keypackages_explainer), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -140,7 +154,7 @@ fun CordnKeyPackagesScreen( SettingsFormBlock { if (coordinators.isEmpty()) { Text( - text = stringRes(R.string.cordn_coordinators_none), + text = stringRes(Res.string.cordn_coordinators_none), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -169,7 +183,7 @@ private fun CoordinatorKeyPackages( var rows by remember(config.pubKey) { mutableStateOf<List<CordnKeyPackageRow>?>(null) } var busy by remember(config.pubKey) { mutableStateOf(false) } var error by remember(config.pubKey) { mutableStateOf<String?>(null) } - val failed = stringRes(R.string.cordn_keypackages_failed) + val failed = stringRes(Res.string.cordn_keypackages_failed) suspend fun reload() { busy = true @@ -204,15 +218,15 @@ private fun CoordinatorKeyPackages( val hasLastResort = loaded.any { it.lastResort } Text( - text = pluralStringRes(LocalContext.current, R.plurals.cordn_keypackages_summary, single, single), + text = pluralStringRes(Res.plurals.cordn_keypackages_summary, single, single), style = MaterialTheme.typography.bodyMedium, ) Text( text = if (hasLastResort) { - stringRes(R.string.cordn_keypackages_last_resort_yes) + stringRes(Res.string.cordn_keypackages_last_resort_yes) } else { - stringRes(R.string.cordn_keypackages_last_resort_no) + stringRes(Res.string.cordn_keypackages_last_resort_no) }, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -225,11 +239,11 @@ private fun CoordinatorKeyPackages( ) { Column(Modifier.padding(12.dp)) { Text( - text = pluralStringRes(LocalContext.current, R.plurals.cordn_keypackages_orphans, orphans.size, orphans.size), + text = pluralStringRes(Res.plurals.cordn_keypackages_orphans, orphans.size, orphans.size), style = MaterialTheme.typography.titleSmall, ) Text( - text = stringRes(R.string.cordn_keypackages_orphans_body), + text = stringRes(Res.string.cordn_keypackages_orphans_body), style = MaterialTheme.typography.bodySmall, ) TextButton(onClick = { @@ -242,7 +256,7 @@ private fun CoordinatorKeyPackages( reload() } }) { - Text(stringRes(R.string.cordn_keypackages_withdraw_orphans)) + Text(stringRes(Res.string.cordn_keypackages_withdraw_orphans)) } } } @@ -251,7 +265,7 @@ private fun CoordinatorKeyPackages( Text( // Above the buttons, because it is the part that cannot be taken // back: §8.4 makes publishing an attributable, permanent record. - text = stringRes(R.string.cordn_keypackages_disclosure), + text = stringRes(Res.string.cordn_keypackages_disclosure), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -274,7 +288,7 @@ private fun CoordinatorKeyPackages( }, enabled = !busy, ) { - Text(stringRes(R.string.cordn_keypackages_publish)) + Text(stringRes(Res.string.cordn_keypackages_publish)) } OutlinedButton( onClick = { @@ -289,7 +303,7 @@ private fun CoordinatorKeyPackages( }, enabled = !busy && !hasLastResort, ) { - Text(stringRes(R.string.cordn_keypackages_publish_last_resort)) + Text(stringRes(Res.string.cordn_keypackages_publish_last_resort)) } if (loaded.isNotEmpty()) { TextButton( @@ -305,7 +319,7 @@ private fun CoordinatorKeyPackages( }, enabled = !busy, ) { - Text(stringRes(R.string.cordn_keypackages_withdraw_all), color = MaterialTheme.colorScheme.error) + Text(stringRes(Res.string.cordn_keypackages_withdraw_all), color = MaterialTheme.colorScheme.error) } } } @@ -313,7 +327,7 @@ private fun CoordinatorKeyPackages( Text( // The rule CordnRuntime.maintainKeyPackages implements, said where // someone can see that it applies to them. - text = stringRes(R.string.cordn_keypackages_topup_note), + text = stringRes(Res.string.cordn_keypackages_topup_note), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnLinkScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnLinkScreen.kt index 86aab6c17c..f4ac911967 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnLinkScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnLinkScreen.kt @@ -45,7 +45,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.cordn.CordnLinkInspection import com.vitorpamplona.amethyst.commons.cordn.ui.CordnExposureCard import com.vitorpamplona.amethyst.commons.resources.Res @@ -60,6 +59,11 @@ import com.vitorpamplona.amethyst.commons.resources.cordn_link_no_coordinator import com.vitorpamplona.amethyst.commons.resources.cordn_link_not_joinable import com.vitorpamplona.amethyst.commons.resources.cordn_link_paste import com.vitorpamplona.amethyst.commons.resources.cordn_link_relays +import com.vitorpamplona.amethyst.commons.resources.cordn_link_request +import com.vitorpamplona.amethyst.commons.resources.cordn_link_request_disclosure +import com.vitorpamplona.amethyst.commons.resources.cordn_link_request_failed +import com.vitorpamplona.amethyst.commons.resources.cordn_link_request_sent +import com.vitorpamplona.amethyst.commons.resources.cordn_link_scan import com.vitorpamplona.amethyst.commons.resources.cordn_link_section_input import com.vitorpamplona.amethyst.commons.resources.cordn_link_title import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -102,8 +106,8 @@ fun CordnLinkScreen( var requesting by remember { mutableStateOf(false) } val runtime = accountViewModel.account.cordnRuntime val scope = rememberCoroutineScope() - val requestFailed = stringRes(R.string.cordn_link_request_failed) - val asked = stringRes(R.string.cordn_link_request_sent) + val requestFailed = stringRes(Res.string.cordn_link_request_failed) + val asked = stringRes(Res.string.cordn_link_request_sent) var scanning by remember { mutableStateOf(false) } val clipboard = LocalClipboardManager.current @@ -163,7 +167,7 @@ fun CordnLinkScreen( Text(stringResource(Res.string.cordn_link_paste)) } OutlinedButton(onClick = { scanning = true }) { - Text(stringRes(R.string.cordn_link_scan)) + Text(stringRes(Res.string.cordn_link_scan)) } if (input.isNotEmpty()) { OutlinedButton( @@ -265,7 +269,7 @@ fun CordnLinkScreen( enabled = !requesting, modifier = Modifier.fillMaxWidth(), ) { - Text(stringRes(R.string.cordn_link_request)) + Text(stringRes(Res.string.cordn_link_request)) } Text( @@ -274,7 +278,7 @@ fun CordnLinkScreen( // KeyPackage under this account's own key and tells // the coordinator this account wants into this // group, whether or not anyone ever answers (§8.4). - text = stringRes(R.string.cordn_link_request_disclosure), + text = stringRes(Res.string.cordn_link_request_disclosure), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnMigrateScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnMigrateScreen.kt index db9b3d4d4e..6c55c2bbb0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnMigrateScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnMigrateScreen.kt @@ -47,9 +47,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.cordn.CordnMigration import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable +import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable_detail import com.vitorpamplona.amethyst.commons.resources.cordn_migrate_cancel import com.vitorpamplona.amethyst.commons.resources.cordn_migrate_cancel_desc import com.vitorpamplona.amethyst.commons.resources.cordn_migrate_code_note @@ -119,8 +120,8 @@ fun CordnMigrateScreen( ) { padding -> if (runtime == null) { EmptyState( - title = stringRes(R.string.cordn_group_unavailable), - description = stringRes(R.string.cordn_group_unavailable_detail), + title = stringRes(Res.string.cordn_group_unavailable), + description = stringRes(Res.string.cordn_group_unavailable_detail), modifier = Modifier.padding(padding), ) return@Scaffold diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index 60879f7b96..8343082ebf 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -353,322 +353,4 @@ <string name="mute_button">ध्वनि सक्रिय। मौन करने के लिए टाँकें</string> <string name="play">चलाएँ</string> <string name="pause">विराम</string> - <string name="backup_conflict_title">आपने अपना %1$s अन्य क्रमक में परिवर्तित किया</string> - <string name="backup_conflict_removed">हटाए गए (%1$s)</string> - <string name="backup_conflict_added">जोडे गए (%1$s)</string> - <string name="backup_conflict_changed">परिवर्तित (%1$s)</string> - <string name="backup_conflict_private_cleared">आपने सभी निजी (रहस्यीकृत) वस्तुओं को हटा दिया।</string> - <string name="backup_conflict_private_changed">आपने निजी (रहस्यीकृत) वस्तुओं का परिवर्तन भी किया। उनकी तुलना यहाँ नहीं की जा सकती।</string> - <string name="backup_conflict_private_added">आपने निजी (रहस्यीकृत) वस्तुओं को जोडा।</string> - <string name="backup_conflict_restore_mine">सुरक्षित संस्करण पुनःस्थापन</string> - <string name="backup_review_saved">इस यन्त्र पर</string> - <string name="backup_review_new">नया संस्करण</string> - <string name="backup_review_not_set">स्थापित नहीं</string> - <string name="backup_conflict_count_removed">%1$s हटाए गए</string> - <string name="backup_conflict_count_added">%1$s जोडे गए</string> - <string name="backup_conflict_count_changed">%1$s परिवर्तित</string> - <plurals name="backup_card_follows_dropped"> - <item quantity="one">आपने %1$d व्यक्ति का अनुगमन हटाया</item> - <item quantity="other">आपने %1$d व्यक्तियों का अनुगमन हटाए</item> - </plurals> - <string name="backup_card_accept">स्वीकार</string> - <string name="backup_review_title_updated">%1$s अद्यतनकृत</string> - <string name="backup_review_title_cleared">%1$s पारित</string> - <string name="backup_review_changed_by_other_app">अन्य क्रमक में परिवर्तित। %1$s</string> - <string name="backup_action_keep_count">रखें %1$s</string> - <string name="backup_action_restore_count">पुनःस्थापन %1$s</string> - <string name="backup_action_remute">पुनःमौन %1$s</string> - <string name="backup_action_rejoin">पुनःजुडें %1$s</string> - <string name="backup_action_restore_profile">परिचय पुनःस्थापध</string> - <string name="backup_action_restore_relays">पुनःप्रसारक पुनःस्थापन</string> - <string name="backup_review_kept_count">%1$s रखे गए</string> - <string name="backup_review_dropped_count">%1$s परित्यक्त</string> - <string name="backup_review_new_count">%1$s नए</string> - <string name="backup_review_tab_dropped">परित्यक्त। %1$s</string> - <string name="backup_review_tab_new">नए। %1$s</string> - <string name="backup_review_tab_edited">सम्पादित। %1$s</string> - <string name="backup_review_search_people">%1$s लोग खोजें</string> - <string name="backup_review_nothing_here">यहाँ कुछ नहीं</string> - <string name="backup_review_unmuted">%1$s मौनावस्था हटाए गए</string> - <string name="backup_review_unmuted_explainer">ये पुनः आपके सूचनावलियों तक अभिगम्य</string> - <string name="backup_review_newly_muted">%1$s नए मौनकृत</string> - <string name="backup_review_words_and_hashtags">शब्द तथा विषयसूचक</string> - <string name="backup_review_names_and_more">%1$s तथा %2$s अधिक</string> - <string name="backup_review_private_wiped_title">आपने सभी निजी वस्तुओं को हटा दिया</string> - <string name="backup_review_private_wiped_body">वे रहस्यीकृत हैं। इसलिए केवल आप देख सकते हैं कि वे क्या थे। पुनःस्थापन से सब लौटके आ जाएँगे।</string> - <string name="backup_review_private_added_title">आपने वस्तुओं को निजी किया</string> - <string name="backup_review_private_added_body">वे अब रहस्यीकृत हैं। जो कि सम्भाव्यतः ऊपर के सार्वजनिक वस्तुओं का गन्तव्य था।</string> - <string name="backup_review_private_changed_title">आपने निजी वस्तुओं का परिवर्तन किया</string> - <string name="backup_review_private_changed_body">वे रहस्यीकृत हैं। इसलिए उनकी तुलना यहाँ नहीं की जा सकती।</string> - <string name="backup_review_gone">चला गया</string> - <string name="backup_review_new_tag">नव्य</string> - <string name="backup_review_dropped_tag">परित्यक्त</string> - <string name="backup_review_demoted_tag">स्थानान्तरित</string> - <string name="backup_review_units_tag">इकाई</string> - <string name="backup_review_left_tag">शेष</string> - <string name="backup_review_renamed_tag">पुनःनामकृत</string> - <string name="backup_review_outbox">निर्गतपेटिका</string> - <string name="backup_review_outbox_explainer">आप जहाँ प्रकाशित करते हैं</string> - <string name="backup_review_inbox">आगतपेटिका</string> - <string name="backup_review_inbox_explainer">जहाँ अन्य आप तक अभिगमन करते हैं</string> - <string name="backup_review_key_replaced">ग्राहक कुंचिका प्रतिस्थापित</string> - <string name="backup_review_key_removed">ग्राहक कुंचिका हटाया गया</string> - <string name="backup_review_key_warning">नटज्साप आगे से इस कुंचिका से बद्ध होंगे। यदि इस यन्त्र में वह नहीं है तो आप सम्भाव्यतः उनको यहाँ नहीं चुका सकते।</string> - <string name="backup_review_left">शेष</string> - <string name="backup_review_joined">जुड गए</string> - <string name="backup_review_renamed">पुनःनामकृत</string> - <string name="backup_review_was_named">पूर्वतः %1$s था</string> - <string name="backup_action_block_again">पुनःबाधित करें %1$s को</string> - <string name="backup_action_restore_feeds">सूचनावलियाँ पुनःस्थापन</string> - <string name="backup_action_restore_topics">विषय पुनःस्थापन</string> - <string name="backup_action_restore_places">स्थल पुनःस्थापन</string> - <string name="backup_action_restore_providers">प्रदाता पुनःस्थापन</string> - <string name="backup_action_restore_wallet">धनकोष पुनःस्थापन</string> - <string name="backup_review_kept">रखे गए</string> - <string name="backup_review_receiving_tag">ग्रहण मध्य</string> - <string name="backup_review_listed_tag">सूचीबद्ध</string> - <string name="backup_review_searching_tag">खोज मध्य</string> - <string name="backup_review_storing_tag">अभिलेखन मध्य</string> - <string name="backup_review_trusted_tag">विश्वसनीय</string> - <string name="backup_review_untrusted_tag">अविश्वसनीय</string> - <string name="backup_review_unblocked">%1$s अबाधित</string> - <string name="backup_review_unblocked_explainer">अमेथिस्ट पुनः उनसे जुडेगा</string> - <string name="backup_review_no_longer_blocked">अब से बाधित नहीं</string> - <string name="backup_review_newly_blocked">नव्यतः बाधित</string> - <string name="backup_review_still_blocked">अभी भी बाधित</string> - <string name="backup_review_allowed_tag">अनुमत</string> - <string name="backup_review_moved_tag">स्थानान्तरित</string> - <string name="backup_review_feed">सूचनावली</string> - <string name="backup_review_topics_gone">विषय हटाए गए</string> - <string name="backup_review_places_gone">स्थल हटाए गए</string> - <string name="backup_review_new_count_caption">नव्य</string> - <string name="backup_review_hashtags_you_follow">विषयसूचक जिनका आप अनुगमन करते हैं</string> - <string name="backup_review_places_you_follow">स्थल जिनका आप अनुगमन करते हैं</string> - <string name="backup_review_services_changed">%2$s में से %1$s सेवाएँ परिवर्तित</string> - <string name="backup_review_services_changed_explainer">वे अब अन्य व्यक्ति से आते है। अथवा किसी से नहीं</string> - <string name="backup_review_service">सेवा</string> - <string name="backup_review_service_rank">श्रेणी</string> - <string name="backup_review_service_followers">अनुचर</string> - <string name="backup_review_service_topics">विषयसूची</string> - <string name="backup_review_trust_warning">श्रेणीयाँ तथा अनुचर गिनती तथा कचरालेख छालन जो आप देखते हैं इन प्रदाताओं से आते हैं।</string> - <plurals name="backup_review_payment_targets_gone"> - <item quantity="one">आपको भुगतान करने की %1$d विधि हटाया गया</item> - <item quantity="other">आपको भुगतान करने की %1$d विधियाँ हटाए गए</item> - </plurals> - <plurals name="backup_review_offers_gone"> - <item quantity="one">%1$d प्रस्ताव हटाया गया</item> - <item quantity="other">%1$d प्रस्ताव हटाए गए</item> - </plurals> - <string name="backup_review_payments_explainer">लोग जो आपको भुगतान करते हैं उसी को देखते हैं जो इस सूची में है।</string> - <string name="backup_review_offer">बोल्ट॰बारह प्रस्ताव</string> - <string name="backup_review_encrypted_cleared">आपने उसे रिक्त किया</string> - <string name="backup_review_encrypted_added">आपने उसे बनाया</string> - <string name="backup_review_encrypted_changed">आपने उसे परिवर्तित किया</string> - <string name="backup_review_wallet_body">आपके धनकोष के टकसाल तथा कुंचिकाएँ आपके लिए रहस्यीकृत हैं। इसलिए उनकी तुलना वस्तु प्रति वस्तु यहाँ नहीं की जा सकती। पुनःस्थापन उन्हीं को लौट डालेगा जिनका अभिलेखन इस यन्त्र ने किया।</string> - <string name="backup_review_concord_body">आपके कोंकोर्ड समुदाय आपके लिए रहस्यीकृत हैं। इसलिए उनकी तुलना यहाँ नहीं की जा सकती। पुनःस्थापन उन्हीं को लौट डालेगा जिनका अभिलेखन इस यन्त्र ने किया।</string> - <plurals name="backup_card_mutes_visible"> - <item quantity="one">आपने %1$d की मौनावस्था हटाया</item> - <item quantity="other">आपने %1$d की मौनावस्था हटाए</item> - </plurals> - <plurals name="backup_card_profile_lost_fields"> - <item quantity="one">आपने %1$d परिचय प्रपत्रस्थान हटाया</item> - <item quantity="other">आपने %1$d परिचय प्रपत्रस्थान हटाए</item> - </plurals> - <plurals name="backup_card_nip65_lost"> - <item quantity="one">आपने %1$d सार्वजनिक पुनःप्रसारक हटाया</item> - <item quantity="other">आपने %1$d सार्वजनिक पुनःप्रसारकों को हटाए</item> - </plurals> - <plurals name="backup_card_dm_relays_lost"> - <item quantity="one">आपने %1$d सीधासन्देश पुनःप्रसारक हटाया</item> - <item quantity="other">आपने %1$d सीधासन्देश पुनःप्रसारकों को हटाए</item> - </plurals> - <plurals name="backup_card_key_package_relays_lost"> - <item quantity="one">आपने %1$d समूहचर्चा कुंचिका पुनःप्रसारक हटाया</item> - <item quantity="other">आपने %1$d समूहचर्चा कुंचिका पुनःप्रसारकों को हटाए</item> - </plurals> - <plurals name="backup_card_search_relays_lost"> - <item quantity="one">आपने %1$d खोज पुनःप्रसारक हटाया</item> - <item quantity="other">आपने %1$d खोज पुनःप्रसारकों को हटाए</item> - </plurals> - <plurals name="backup_card_indexer_relays_lost"> - <item quantity="one">आपने %1$d अनुक्रमणीकार पुनःप्रसारक हटाया</item> - <item quantity="other">आपने %1$d अनुक्रमणीकार पुनःप्रसारकों को हटाए</item> - </plurals> - <plurals name="backup_card_relay_feeds_lost"> - <item quantity="one">आपने %1$d पुनःप्रसारक सूचनावली हटाया</item> - <item quantity="other">आपने %1$d पुनःप्रसारक सूचनावलियाँ हटाए</item> - </plurals> - <plurals name="backup_card_private_outbox_lost"> - <item quantity="one">आपने %1$d निजी पुनःप्रसारक हटाया</item> - <item quantity="other">आपने %1$d निजी पुनःप्रसारकों को हटाए</item> - </plurals> - <plurals name="backup_card_trusted_lost"> - <item quantity="one">आपने %1$d पुनःप्रसारक पर विश्वास करना रोक दिया</item> - <item quantity="other">आपने %1$d पुनःप्रसारकों पर विश्वास करना रोक दिए</item> - </plurals> - <plurals name="backup_card_unblocked"> - <item quantity="one">आपने %1$d पुनःप्रसारक अबाधित किया</item> - <item quantity="other">आपने %1$d पुनःप्रसारकों को अबाधित किए</item> - </plurals> - <plurals name="backup_card_leave_chats"> - <item quantity="one">आपने %1$d सार्वजनिक चर्चा छोड दिया</item> - <item quantity="other">आपने %1$d सार्वजनिक चर्चाओं को छोड दिए</item> - </plurals> - <plurals name="backup_card_leave_communities"> - <item quantity="one">आपने %1$d समुदाय छोड दिया</item> - <item quantity="other">आपने %1$d समुदायों को छोड दिए</item> - </plurals> - <plurals name="backup_card_leave_rooms"> - <item quantity="one">आपने %1$d चर्चाशाला छोड दिया</item> - <item quantity="other">आपने %1$d चर्चाशालाओं को छोड दिए</item> - </plurals> - <plurals name="backup_card_leave_groups"> - <item quantity="one">आपने %1$d समूह छोड दिया</item> - <item quantity="other">आपने %1$d समूहों को छोड दिए</item> - </plurals> - <plurals name="backup_card_feeds_removed"> - <item quantity="one">आपने %1$d इष्ट सूचनावली हटाया</item> - <item quantity="other">आपने %1$d इष्ट सूचनावलियों को हटाए</item> - </plurals> - <plurals name="backup_card_hashtags_unfollowed"> - <item quantity="one">आपने %1$d विषयसूचक का अनुगमन हटाया</item> - <item quantity="other">आपने %1$d विषयसूचकों का अनुगमन हटाए</item> - </plurals> - <plurals name="backup_card_places_unfollowed"> - <item quantity="one">आपने %1$d स्थल का अनुगमन हटाया</item> - <item quantity="other">आपने %1$d स्थलों का अनुगमन हटाए</item> - </plurals> - <plurals name="backup_card_trust_services_changed"> - <item quantity="one">आपने %1$d विश्वाससेवा प्रदाता परिवर्तित किया</item> - <item quantity="other">आपने %1$d विश्वाससेवाओं के प्रदाता को परिवर्तित किया</item> - </plurals> - <plurals name="backup_card_nutzap_mints_removed"> - <item quantity="one">आपने %1$d नटज्साप टकसाल हटाया</item> - <item quantity="other">आपने %1$d नटज्साप टकसालों को हटाए</item> - </plurals> - <plurals name="backup_card_payment_targets_removed"> - <item quantity="one">आपने %1$d भुगतान प्राप्त करने की विधि हटाया</item> - <item quantity="other">आपने %1$d भुगतान प्राप्त करने के विधियों को हटाए</item> - </plurals> - <plurals name="backup_card_offers_removed"> - <item quantity="one">आपने %1$d बोल्ट॰बारह प्रस्ताव हटाया</item> - <item quantity="other">आपने %1$d बोल्ट॰बारह प्रस्तावों को हटाए</item> - </plurals> - <plurals name="backup_card_and_added"> - <item quantity="one">%1$s तथा %2$d जोडा</item> - <item quantity="other">%1$s तथा %2$d जोडे</item> - </plurals> - <plurals name="backup_card_and_followed"> - <item quantity="one">%1$s तथा %2$d का अनुगमन किया</item> - <item quantity="other">%1$s तथा %2$d का अनुगमन किए</item> - </plurals> - <plurals name="backup_card_and_muted"> - <item quantity="one">%1$s तथा %2$d को मौनकृत किया</item> - <item quantity="other">%1$s तथा %2$d को मौनकृत किए</item> - </plurals> - <plurals name="backup_card_and_joined"> - <item quantity="one">%1$s तथा %2$d से जुडे</item> - <item quantity="other">%1$s तथा %2$d से जुडे</item> - </plurals> - <plurals name="backup_card_and_blocked"> - <item quantity="one">%1$s तथा %2$d बाधित किया</item> - <item quantity="other">%1$s तथा %2$d को बाधित किए</item> - </plurals> - <string name="backup_card_app_settings">आपके अमेथिस्ट समचरणीकृत क्रमक स्थापना विकल्पों को परिवर्तित किया गया एक अन्य क्रमक में और वह अमेथिस्ट नहीं था। इससे समस्याएँ उत्पन्न हो सकते हैं।</string> - <string name="backup_card_nutzap_key_replaced">आपने अपनी नटज्साप कुंचिका का प्रतिस्थापन किया</string> - <string name="backup_card_nutzap_key_removed">आपने अपनी नटज्साप कुंचिका को हटाया</string> - <string name="backup_card_wallet_emptied">आपने अपना काशयू धनकोष रिक्त किया</string> - <string name="backup_card_wallet_rewritten">आपने अपने काशयू धनकोष को परिवर्तित किया</string> - <string name="backup_card_concord_emptied">आपने अपनी कोंकोर्ड सूची रिक्त की</string> - <string name="backup_card_concord_rewritten">आपने अपनी कोंकोर्ड सूची का परिवर्तन किया</string> - <string name="backup_conflict_keep_new">नया संस्करण का उपयोग करें</string> - <string name="backup_conflict_relay_read_only">पठनेवशक्य</string> - <string name="backup_conflict_relay_write_only">लेखनेवशक्य</string> - <string name="backup_conflict_relay_read_write">पठन तथा लेखन</string> - <string name="backup_type_profile">परिचय</string> - <string name="backup_type_profile_explainer">आपका नाम चित्र चरित तथा भुगतान पते। जैसे सब को दिखते हैं वैसे।</string> - <string name="backup_type_follow_list">अनुचरण सूची</string> - <string name="backup_type_follow_list_explainer">निर्णय करता है किसके पत्र आपकी मुख्य सूचनावली में प्रकट होंगे।</string> - <string name="backup_type_mute_list">मौन सूची</string> - <string name="backup_type_mute_list_explainer">छिपाता है लोग शब्द विषयसूचक तथा सूत्र उन सब में से जो आप देखते हैं।</string> - <string name="backup_type_outbox_inbox_relays">सार्वजनिक पुनःप्रसारक सूची</string> - <string name="backup_type_outbox_inbox_relays_explainer">सबको बताता है आपके पत्र कहाँ प्राप्त करें तथा आपको उत्तर तथा उल्लेखों को कहाँ भेजें।</string> - <string name="backup_type_dm_relays">सीधासन्देश पुनःप्रसारक सूची</string> - <string name="backup_type_dm_relays_explainer">जहाँ अन्य आपके निजी सन्देश भेजते हैं।</string> - <string name="backup_type_key_package_relays">समूहचर्चा कुंचिका पुनःप्रसारक</string> - <string name="backup_type_key_package_relays_explainer">जहाँ अन्य उन कुंचिकाओं को प्राप्त करते हैं आपको रहस्यीकृत समूहचर्चाओं में जोडने के लिए।</string> - <string name="backup_type_search_relays">खोज पुनःप्रसारक सूची</string> - <string name="backup_type_search_relays_explainer">उपयुक्त पुनःप्रसारक जब आप खोजते हैं।</string> - <string name="backup_type_indexer_relays">अनुक्रमणीकार पुनःप्रसारक सूची</string> - <string name="backup_type_indexer_relays_explainer">पुनःप्रसारक उपयुक्त अन्य लोगों के परिचय तथा पुनःप्रसारक सूचियों को प्राप्त करने के लिए।</string> - <string name="backup_type_relay_feeds">इष्ट पुनःप्रसारक सूचनावलियाँ</string> - <string name="backup_type_relay_feeds_explainer">पुनःप्रसारक जिनका आप वीक्षण करते हैं सूचनावलियों के रूप में।</string> - <string name="backup_type_blocked_relays">बाधित पुनःप्रसारक सूची</string> - <string name="backup_type_blocked_relays_explainer">पुनःप्रसारक जिनसे यह क्रमक कभी भी संयोजन नहीं करता।</string> - <string name="backup_type_trusted_relays">विश्वसनीय पुनःप्रसारक सूची</string> - <string name="backup_type_trusted_relays_explainer">पुनःप्रसारक जिनपर आप पर्याप्त विश्वास करते हैं बिना पूछे उनसे संयोजन करने के लिए।</string> - <string name="backup_type_private_outbox_relays">निजी पुनःप्रसारक सूची</string> - <string name="backup_type_private_outbox_relays_explainer">जहाँ आपके पाण्डुलिपियाँ तथा निजी जानकारी रखे जाते हैं।</string> - <string name="backup_type_app_settings">समचरणीकृत क्रमक स्थापना विकल्प</string> - <string name="backup_type_app_settings_explainer">अमेथिस्ट स्थापना विकल्प समचरणीकृत आपके यन्त्रों में। वे रहस्यीकृत हैं। इसलिए केवल उनकी उपस्थिति की तुलना की जा सकती है।</string> - <string name="backup_type_public_chats">सार्वजनिक चर्चा सूची</string> - <string name="backup_type_public_chats_explainer">सार्वजनिक चर्चाएँ जिनसे आप जुडे।</string> - <string name="backup_type_communities">समुदाय सूची</string> - <string name="backup_type_communities_explainer">समुदाय जिनसे आप जुडे।</string> - <string name="backup_type_hashtags">अनुचरित विषयसूचक</string> - <string name="backup_type_hashtags_explainer">विषयसूचक जिससे युक्त पत्र आपकी मुख्य सूचनावली में प्रकट होंगे।</string> - <string name="backup_type_geohashes">अनुचरित स्थान</string> - <string name="backup_type_geohashes_explainer">स्थान जहाँ से पत्र आपकी मुख्य सूचनावली में प्रकट होंगे।</string> - <string name="backup_type_favorite_algo_feeds">इष्ट सूचनावली सूची</string> - <string name="backup_type_favorite_algo_feeds_explainer">कलनविध्यात्मक सूचनावलियाँ जिनको आपने अभिलेखित किया।</string> - <string name="backup_type_ephemeral_chats">अस्थायी चर्चा सूची</string> - <string name="backup_type_ephemeral_chats_explainer">अस्थायी चर्चा शालाएँ जिनसे आप जुडे।</string> - <string name="backup_type_relay_groups">समूह सूची</string> - <string name="backup_type_relay_groups_explainer">पुनःप्रसारक आधारित समूह जिनसे आप जुडे।</string> - <string name="backup_type_concord_communities">कोंकोर्ड समुदाय सूची</string> - <string name="backup_type_concord_communities_explainer">कोंकोर्ड समुदाय जिनसे आप जुडे। वे रहस्यीकृत हैं। इसलिए केवल उनकी उपस्थिति की तुलना की जा सकती है।</string> - <string name="backup_type_trust_providers">विश्वाससेवा प्रदाता सूची</string> - <string name="backup_type_trust_providers_explainer">सेवाएँ जिनपर आप विश्वास करते है आपके लिए लोगों का सत्यापन तथा श्रेणीकरण करने के लिए।</string> - <string name="backup_type_cashu_wallet">काशयू धनकोष</string> - <string name="backup_type_cashu_wallet_explainer">आपके ईकाश धनकोष स्थापना विकल्प तथा टकसाल। वे रहस्यीकृत हैं। इसलिए केवल उनकी उपस्थिति की तुलना की जा सकती है।</string> - <string name="backup_type_nutzap_info">नटज्साप प्राप्ती स्थापना विकल्प</string> - <string name="backup_type_nutzap_info_explainer">टकसाल तथा कुंचिका जिनका उपयोग अन्य करते हैं आपको नटज्साप भेजने के लिए।</string> - <string name="backup_type_payment_targets">भुगतान पते</string> - <string name="backup_type_payment_targets_explainer">विधियाँ जिनसे अन्य लोग आपको भुगतान कर सके।</string> - <string name="backup_type_bolt12_offers">बोल्ट॰बारह प्रस्ताव</string> - <string name="backup_type_bolt12_offers_explainer">लैटनिंग प्रस्ताव जिनका उपयोग करके अन्य लोग आपको भुगतान करते हैं।</string> - <string name="backup_type_other">सूची</string> - <string name="backup_type_other_explainer">इस यन्त्र पर अभिलेखित सूची।</string> - <string name="backup_entry_profile_field">परिचय प्रपत्रस्थान</string> - <string name="backup_entry_person">लोग</string> - <string name="backup_entry_hashtag">विषयसूचक</string> - <string name="backup_entry_word">शब्द</string> - <string name="backup_entry_thread">सूत्र</string> - <string name="backup_entry_public_chat">सार्वजनिक चर्चाएँ</string> - <string name="backup_entry_community">समुदाय</string> - <string name="backup_entry_algo_feed">सूचनावलियाँ</string> - <string name="backup_entry_location">स्थान</string> - <string name="backup_entry_relay">पुनःप्रसारक</string> - <string name="backup_entry_relay_group">समूह</string> - <string name="backup_entry_chat_room">चर्चा शालाएँ</string> - <string name="backup_entry_trust_provider">विश्वाससेवा प्रदाता</string> - <string name="backup_entry_mint">टकसाल</string> - <string name="backup_entry_nutzap_key">नटज्साप कुंचिकाएँ</string> - <string name="backup_entry_payment_target">भुगतान पते</string> - <string name="backup_entry_offer">प्रस्ताव</string> - <string name="backup_entry_identity_claim">सम्बन्धित व्यक्तिगण</string> - <string name="backup_entry_other">अन्य प्रविष्टियाँ</string> - <string name="backup_profile_field_name">प्रयोक्तानाम</string> - <string name="backup_profile_field_display_name">प्रदर्शन नाम</string> - <string name="backup_profile_field_about">जानकारी</string> - <string name="backup_profile_field_picture">चित्र</string> - <string name="backup_profile_field_banner">ध्वज</string> - <string name="backup_profile_field_website">जालस्थान</string> - <string name="backup_profile_field_nip05">नोस्टर पता</string> - <string name="backup_profile_field_lud16">लैटनिंग पता</string> - <string name="backup_profile_field_lud06">लैटनिंग जालपता</string> - <string name="backup_profile_field_pronouns">सर्वनाम</string> - <string name="backup_profile_field_birthday">जन्मदिन</string> - <string name="backup_profile_field_clink_offer">क्लिंक॰ प्रस्ताव</string> - <string name="backup_profile_field_bot">क्रमचर चिह्न</string> </resources> diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index e7a9da119d..37bd743202 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -353,322 +353,4 @@ <string name="mute_button">Hang bekapcsolva. Koppintson a némításhoz</string> <string name="play">Lejátszás</string> <string name="pause">Szüneteltetés</string> - <string name="backup_conflict_title">Ön módosította a következőt: %1$s, egy másik alkalmazásban</string> - <string name="backup_conflict_removed">Eltávolítva (%1$s)</string> - <string name="backup_conflict_added">Hozzáadva (%1$s)</string> - <string name="backup_conflict_changed">Módosítva (%1$s)</string> - <string name="backup_conflict_private_cleared">Ön eltávolította az összes privát (titkosított) elemet.</string> - <string name="backup_conflict_private_changed">Ön privát (titkosított) elemeket is módosított. Ezek itt nem hasonlíthatók össze.</string> - <string name="backup_conflict_private_added">Ön privát (titkosított) elemeket adott hozzá.</string> - <string name="backup_conflict_restore_mine">Mentett verzió visszaállítása</string> - <string name="backup_review_saved">Ezen az eszközön</string> - <string name="backup_review_new">Új verzió</string> - <string name="backup_review_not_set">Nincs beállítva</string> - <string name="backup_conflict_count_removed">%1$s eltávolítva</string> - <string name="backup_conflict_count_added">%1$s hozzáadva</string> - <string name="backup_conflict_count_changed">%1$s módosítva</string> - <plurals name="backup_card_follows_dropped"> - <item quantity="one">%1$d személyt nem követ többé</item> - <item quantity="other">%1$d személyt nem követ többé</item> - </plurals> - <string name="backup_card_accept">Elfogadás</string> - <string name="backup_review_title_updated">%1$s frissítve</string> - <string name="backup_review_title_cleared">%1$s törölve</string> - <string name="backup_review_changed_by_other_app">Módosítva egy másik alkalmazásban · %1$s</string> - <string name="backup_action_keep_count">%1$s megtartása</string> - <string name="backup_action_restore_count">%1$s visszaállítása</string> - <string name="backup_action_remute">%1$s újbóli némítása</string> - <string name="backup_action_rejoin">Újbóli csatlakozás ehhez: %1$s</string> - <string name="backup_action_restore_profile">Profil visszaállítása</string> - <string name="backup_action_restore_relays">Átjátszók visszaállítása</string> - <string name="backup_review_kept_count">%1$s megtartva</string> - <string name="backup_review_dropped_count">%1$s elvetve</string> - <string name="backup_review_new_count">%1$s új</string> - <string name="backup_review_tab_dropped">Elvetve · %1$s</string> - <string name="backup_review_tab_new">Új · %1$s</string> - <string name="backup_review_tab_edited">Szerkesztve · %1$s</string> - <string name="backup_review_search_people">%1$s személy keresése</string> - <string name="backup_review_nothing_here">Itt nincs semmi</string> - <string name="backup_review_unmuted">%1$s némítása feloldva</string> - <string name="backup_review_unmuted_explainer">Ezek újra megjelenhetnek a hírfolyamaiban</string> - <string name="backup_review_newly_muted">%1$s újonnan némítva</string> - <string name="backup_review_words_and_hashtags">Szavak és kulcsszavak</string> - <string name="backup_review_names_and_more">%1$s és %2$s további</string> - <string name="backup_review_private_wiped_title">Ön eltávolította az összes privát elemet</string> - <string name="backup_review_private_wiped_body">Titkosítva vannak, így csak Ön láthatja, mik voltak. A visszaállítás mindet visszahozza.</string> - <string name="backup_review_private_added_title">Ön priváttá tette az elemeket</string> - <string name="backup_review_private_added_body">Most már titkosítva vannak, valószínűleg ide kerültek a fenti nyilvános elemek.</string> - <string name="backup_review_private_changed_title">Ön módosította a privát elemeket</string> - <string name="backup_review_private_changed_body">Titkosítva vannak, így itt nem hasonlíthatók össze.</string> - <string name="backup_review_gone">Eltűnt</string> - <string name="backup_review_new_tag">Új</string> - <string name="backup_review_dropped_tag">Elvetve</string> - <string name="backup_review_demoted_tag">Áthelyezve</string> - <string name="backup_review_units_tag">Egységek</string> - <string name="backup_review_left_tag">Elhagyva</string> - <string name="backup_review_renamed_tag">Átnevezve</string> - <string name="backup_review_outbox">Kimenő</string> - <string name="backup_review_outbox_explainer">ahol közzétesz</string> - <string name="backup_review_inbox">Bejövő</string> - <string name="backup_review_inbox_explainer">ahol mások elérik Önt</string> - <string name="backup_review_key_replaced">Fogadási kulcs lecserélve</string> - <string name="backup_review_key_removed">Fogadási kulcs eltávolítva</string> - <string name="backup_review_key_warning">A nutzapok ehhez az új kulcshoz lesznek kötve. Ha ez az eszköz nem rendelkezik vele, lehet, hogy itt nem tudja beváltani őket.</string> - <string name="backup_review_left">elhagyta a csoportot</string> - <string name="backup_review_joined">csatlakozott</string> - <string name="backup_review_renamed">átnevezte</string> - <string name="backup_review_was_named">„%1$s” volt</string> - <string name="backup_action_block_again">%1$s újbóli letiltása</string> - <string name="backup_action_restore_feeds">Hírfolyamok visszaállítása</string> - <string name="backup_action_restore_topics">Témák visszaállítása</string> - <string name="backup_action_restore_places">Helyek visszaállítása</string> - <string name="backup_action_restore_providers">Szolgáltatók visszaállítása</string> - <string name="backup_action_restore_wallet">Pénztárca visszaállítása</string> - <string name="backup_review_kept">megtartva</string> - <string name="backup_review_receiving_tag">Fogadás</string> - <string name="backup_review_listed_tag">Listázva</string> - <string name="backup_review_searching_tag">Keresés</string> - <string name="backup_review_storing_tag">Tárolás</string> - <string name="backup_review_trusted_tag">Megbízható</string> - <string name="backup_review_untrusted_tag">Nem megbízható</string> - <string name="backup_review_unblocked">%1$s letiltása feloldva</string> - <string name="backup_review_unblocked_explainer">Az Amethyst újra kapcsolódni fog hozzájuk</string> - <string name="backup_review_no_longer_blocked">Már nincs letiltva</string> - <string name="backup_review_newly_blocked">Újonnan letiltva</string> - <string name="backup_review_still_blocked">Továbbra is le van tiltva</string> - <string name="backup_review_allowed_tag">Engedélyezve</string> - <string name="backup_review_moved_tag">Áthelyezve</string> - <string name="backup_review_feed">Hírfolyam</string> - <string name="backup_review_topics_gone">eltűnt témák</string> - <string name="backup_review_places_gone">eltűnt helyek</string> - <string name="backup_review_new_count_caption">új</string> - <string name="backup_review_hashtags_you_follow">Ön által követett kulcsszavak</string> - <string name="backup_review_places_you_follow">Ön által követett helyek</string> - <string name="backup_review_services_changed">%2$s szolgáltatásból %1$s módosult</string> - <string name="backup_review_services_changed_explainer">Most már valaki mástól vagy senkitől sem származnak</string> - <string name="backup_review_service">Szolgáltatás</string> - <string name="backup_review_service_rank">Rang</string> - <string name="backup_review_service_followers">Követők</string> - <string name="backup_review_service_topics">Témák</string> - <string name="backup_review_trust_warning">A látott rangok, követőszámok és a kéretlen tartalomszűrés ezektől a szolgáltatóktól származnak.</string> - <plurals name="backup_review_payment_targets_gone"> - <item quantity="one">%1$d fizetési mód eltávolítva</item> - <item quantity="other">%1$d fizetési mód eltávolítva</item> - </plurals> - <plurals name="backup_review_offers_gone"> - <item quantity="one">%1$d ajánlat eltávolítva</item> - <item quantity="other">%1$d ajánlat eltávolítva</item> - </plurals> - <string name="backup_review_payments_explainer">A fizető emberek csak azt látják, ami ebben a listában van.</string> - <string name="backup_review_offer">BOLT12 ajánlat</string> - <string name="backup_review_encrypted_cleared">Ön kiürítette</string> - <string name="backup_review_encrypted_added">Ön hozta létre</string> - <string name="backup_review_encrypted_changed">Ön módosította</string> - <string name="backup_review_wallet_body">A saját pénztárca pénzverdéi és kulcsai Önhöz vannak titkosítva, így itt nem hasonlíthatók össze tételesen. A visszaállítás pontosan azt hozza vissza, amit ez az eszköz mentett.</string> - <string name="backup_review_concord_body">A Concord közösségei Önhöz vannak titkosítva, így itt nem hasonlíthatók össze. A visszaállítás pontosan azt hozza vissza, amit ez az eszköz mentett.</string> - <plurals name="backup_card_mutes_visible"> - <item quantity="one">Ön %1$d dolog némítását oldotta fel</item> - <item quantity="other">%1$d dolog némítását oldotta fel</item> - </plurals> - <plurals name="backup_card_profile_lost_fields"> - <item quantity="one">Ön %1$d profilmezőt távolított el</item> - <item quantity="other">%1$d profilmezőt távolított el</item> - </plurals> - <plurals name="backup_card_nip65_lost"> - <item quantity="one">Ön %1$d nyilvános átjátszót távolított el</item> - <item quantity="other">%1$d nyilvános átjátszót távolított el</item> - </plurals> - <plurals name="backup_card_dm_relays_lost"> - <item quantity="one">Ön %1$d privát üzenetátjátszót távolított el</item> - <item quantity="other">%1$d privát üzenet átjátszót távolított el</item> - </plurals> - <plurals name="backup_card_key_package_relays_lost"> - <item quantity="one">Ön %1$d csoportos csevegési kulcsátjátszót távolított el</item> - <item quantity="other">%1$d csoportcsevegés kulcs átjátszót távolított el</item> - </plurals> - <plurals name="backup_card_search_relays_lost"> - <item quantity="one">Ön %1$d kereső átjátszót távolított el</item> - <item quantity="other">%1$d kereső átjátszót távolított el</item> - </plurals> - <plurals name="backup_card_indexer_relays_lost"> - <item quantity="one">Ön %1$d indexelő átjátszót távolított el</item> - <item quantity="other">%1$d indexelő átjátszót távolított el</item> - </plurals> - <plurals name="backup_card_relay_feeds_lost"> - <item quantity="one">Ön %1$d átjátszó-hírfolyamot távolított el</item> - <item quantity="other">%1$d átjátszó hírfolyamot távolított el</item> - </plurals> - <plurals name="backup_card_private_outbox_lost"> - <item quantity="one">Ön %1$d privát átjátszót távolított el</item> - <item quantity="other">%1$d privát átjátszót távolított el</item> - </plurals> - <plurals name="backup_card_trusted_lost"> - <item quantity="one">Ön %1$d átjátszóban nem bízik többé</item> - <item quantity="other">%1$d átjátszóban nem bízik többé</item> - </plurals> - <plurals name="backup_card_unblocked"> - <item quantity="one">Ön %1$d átjátszó letiltását oldotta fel</item> - <item quantity="other">%1$d átjátszó blokkolását oldotta fel</item> - </plurals> - <plurals name="backup_card_leave_chats"> - <item quantity="one">Ön %1$d nyilvános csevegést hagyott el</item> - <item quantity="other">%1$d nyilvános csevegést hagyott el</item> - </plurals> - <plurals name="backup_card_leave_communities"> - <item quantity="one">Ön %1$d közösséget hagyott el</item> - <item quantity="other">%1$d közösséget hagyott el</item> - </plurals> - <plurals name="backup_card_leave_rooms"> - <item quantity="one">Ön %1$d csevegőszobát hagyott el</item> - <item quantity="other">%1$d csevegőszobát hagyott el</item> - </plurals> - <plurals name="backup_card_leave_groups"> - <item quantity="one">Ön %1$d csoportot hagyott el</item> - <item quantity="other">%1$d csoportot hagyott el</item> - </plurals> - <plurals name="backup_card_feeds_removed"> - <item quantity="one">Ön %1$d kedvenc hírfolyamot távolított el</item> - <item quantity="other">%1$d kedvenc hírfolyamot távolított el</item> - </plurals> - <plurals name="backup_card_hashtags_unfollowed"> - <item quantity="one">Ön %1$d kulcsszót nem követ többé</item> - <item quantity="other">%1$d hashtaget nem követ többé</item> - </plurals> - <plurals name="backup_card_places_unfollowed"> - <item quantity="one">Ön %1$d helyet nem követ többé</item> - <item quantity="other">%1$d helyet nem követ többé</item> - </plurals> - <plurals name="backup_card_trust_services_changed"> - <item quantity="one">Ön %1$d bizalmi szolgáltatás szolgáltatóját módosította</item> - <item quantity="other">%1$d bizalmi szolgáltatás szolgáltatóját változtatta meg</item> - </plurals> - <plurals name="backup_card_nutzap_mints_removed"> - <item quantity="one">Ön %1$d nutzap pénzverdét távolított el</item> - <item quantity="other">%1$d nutzap pénzverdét távolított el</item> - </plurals> - <plurals name="backup_card_payment_targets_removed"> - <item quantity="one">Ön %1$d fizetési módot távolított el</item> - <item quantity="other">%1$d fizetési módot távolított el</item> - </plurals> - <plurals name="backup_card_offers_removed"> - <item quantity="one">Ön %1$d BOLT12 ajánlatot távolított el</item> - <item quantity="other">%1$d BOLT12 ajánlatot távolított el</item> - </plurals> - <plurals name="backup_card_and_added"> - <item quantity="one">%1$s és %2$d hozzáadva</item> - <item quantity="other">%1$s és %2$d hozzáadva</item> - </plurals> - <plurals name="backup_card_and_followed"> - <item quantity="one">%1$s és %2$d követve</item> - <item quantity="other">%1$s és %2$d követve</item> - </plurals> - <plurals name="backup_card_and_muted"> - <item quantity="one">%1$s és %2$d némítva</item> - <item quantity="other">%1$s és %2$d némítva</item> - </plurals> - <plurals name="backup_card_and_joined"> - <item quantity="one">%1$s és %2$d csatlakozva</item> - <item quantity="other">%1$s és %2$d csatlakozva</item> - </plurals> - <plurals name="backup_card_and_blocked"> - <item quantity="one">%1$s és %2$d letiltva</item> - <item quantity="other">%1$s és %2$d letiltva</item> - </plurals> - <string name="backup_card_app_settings">A szinkronizált Amethyst alkalmazásbeállításai egy másik, nem Amethyst alkalmazásban módosultak. Ez problémákat okozhat.</string> - <string name="backup_card_nutzap_key_replaced">Ön lecserélte a nutzap kulcsát</string> - <string name="backup_card_nutzap_key_removed">Ön eltávolította a nutzap kulcsát</string> - <string name="backup_card_wallet_emptied">Ön kiürítette a Cashu pénztárcáját</string> - <string name="backup_card_wallet_rewritten">Ön módosította a Cashu pénztárcáját</string> - <string name="backup_card_concord_emptied">Ön kiürítette a Concord listáját</string> - <string name="backup_card_concord_rewritten">Ön módosította a Concord listáját</string> - <string name="backup_conflict_keep_new">Új verzió használata</string> - <string name="backup_conflict_relay_read_only">csak olvasható</string> - <string name="backup_conflict_relay_write_only">csak írható</string> - <string name="backup_conflict_relay_read_write">olvasás és írás</string> - <string name="backup_type_profile">profil</string> - <string name="backup_type_profile_explainer">Az Ön neve, képe, bemutatkozása és fizetési címei, ahogy mindenki látja őket.</string> - <string name="backup_type_follow_list">követési lista</string> - <string name="backup_type_follow_list_explainer">Meghatározza, kinek a bejegyzései jelenjenek meg a kezdőlapon.</string> - <string name="backup_type_mute_list">némítási lista</string> - <string name="backup_type_mute_list_explainer">Elrejti a személyeket, szavakat, kulcsszavakat és beszélgetéseket mindenből, amit lát.</string> - <string name="backup_type_outbox_inbox_relays">nyilvános átjátszólista</string> - <string name="backup_type_outbox_inbox_relays_explainer">Megmondja mindenkinek, hol találhatók a bejegyzései, és hová küldjék a válaszokat és említéseket.</string> - <string name="backup_type_dm_relays">privát üzenet átjátszólista</string> - <string name="backup_type_dm_relays_explainer">Ahová mások kézbesítik a privát üzeneteit.</string> - <string name="backup_type_key_package_relays">csoportos csevegési kulcsátjátszók</string> - <string name="backup_type_key_package_relays_explainer">Ahol mások megtalálják a kulcsokat, hogy titkosított csoportos csevegésekhez adhassák Önt.</string> - <string name="backup_type_search_relays">kereső átjátszólista</string> - <string name="backup_type_search_relays_explainer">Kereséskor használt átjátszók.</string> - <string name="backup_type_indexer_relays">indexelő átjátszólista</string> - <string name="backup_type_indexer_relays_explainer">Azok az átjátszók, amelyeket mások profiljainak és átjátszólistáinak megtalálásához használnak.</string> - <string name="backup_type_relay_feeds">kedvenc átjátszóhírfolyamok</string> - <string name="backup_type_relay_feeds_explainer">Olyan átjátszók, amelyeket hírfolyamként böngész.</string> - <string name="backup_type_blocked_relays">letiltott átjátszólista</string> - <string name="backup_type_blocked_relays_explainer">Átjátszók, amelyekhez ez az alkalmazás soha nem kapcsolódik.</string> - <string name="backup_type_trusted_relays">megbízható átjátszólista</string> - <string name="backup_type_trusted_relays_explainer">Átjátszók, amelyekhez kérdezés nélkül kapcsolódhat.</string> - <string name="backup_type_private_outbox_relays">privát átjátszólista</string> - <string name="backup_type_private_outbox_relays_explainer">Ahol a piszkozatai és privát adatai tárolódnak.</string> - <string name="backup_type_app_settings">szinkronizált alkalmazásbeállítások</string> - <string name="backup_type_app_settings_explainer">Az eszközök között szinkronizált Amethyst-beállítások. Titkosítva vannak, így csak a jelenlétük hasonlítható össze.</string> - <string name="backup_type_public_chats">nyilvános csevegési lista</string> - <string name="backup_type_public_chats_explainer">Nyilvános csevegések, amelyekhez csatlakozott.</string> - <string name="backup_type_communities">közösséglista</string> - <string name="backup_type_communities_explainer">Közösségek, amelyekhez csatlakozott.</string> - <string name="backup_type_hashtags">követett kulcsszavak</string> - <string name="backup_type_hashtags_explainer">Kulcsszavak, amelyek bejegyzései megjelennek a hírfolyamban.</string> - <string name="backup_type_geohashes">követett helyek</string> - <string name="backup_type_geohashes_explainer">Helyek, amelyek bejegyzései megjelennek a hírfolyamaiban.</string> - <string name="backup_type_favorite_algo_feeds">kedvenc hírfolyamlista</string> - <string name="backup_type_favorite_algo_feeds_explainer">Mentett algoritmus-hírfolyamok.</string> - <string name="backup_type_ephemeral_chats">rövid életű csevegési lista</string> - <string name="backup_type_ephemeral_chats_explainer">Rövid életű csevegőszobák, amelyekhez csatlakozott.</string> - <string name="backup_type_relay_groups">csoportlista</string> - <string name="backup_type_relay_groups_explainer">Átjátszó-alapú csoportok, amelyekhez csatlakozott.</string> - <string name="backup_type_concord_communities">Concord közösséglista</string> - <string name="backup_type_concord_communities_explainer">A Concord közösségek, amelyekhez csatlakozott. Titkosítva vannak, így csak a jelenlétük hasonlítható össze.</string> - <string name="backup_type_trust_providers">bizalmi szolgáltatólista</string> - <string name="backup_type_trust_providers_explainer">A szolgáltatások, amelyekben megbízik, hogy rangsorolják és ellenőrizzék Önnek az embereket.</string> - <string name="backup_type_cashu_wallet">Cashu pénztárca</string> - <string name="backup_type_cashu_wallet_explainer">Az ecash pénztárca beállításai és pénzverdéi. Titkosítva vannak, így csak a jelenlétük hasonlítható össze.</string> - <string name="backup_type_nutzap_info">nutzap fogadási beállítások</string> - <string name="backup_type_nutzap_info_explainer">A pénzverdék és a kulcs, amelyeket mások a nutzapok küldéséhez használnak az Ön számára.</string> - <string name="backup_type_payment_targets">fizetési címek</string> - <string name="backup_type_payment_targets_explainer">A módok, ahogy mások fizethetnek Önnek.</string> - <string name="backup_type_bolt12_offers">BOLT12 ajánlatok</string> - <string name="backup_type_bolt12_offers_explainer">A lightning ajánlatok, amelyeket mások a fizetéshez használnak az Ön számára.</string> - <string name="backup_type_other">lista</string> - <string name="backup_type_other_explainer">Egy listaelem ezen az eszközön mentve.</string> - <string name="backup_entry_profile_field">Profilmezők</string> - <string name="backup_entry_person">Személyek</string> - <string name="backup_entry_hashtag">Kulcsszavak</string> - <string name="backup_entry_word">Szavak</string> - <string name="backup_entry_thread">Beszélgetések</string> - <string name="backup_entry_public_chat">Nyilvános csevegések</string> - <string name="backup_entry_community">Közösségek</string> - <string name="backup_entry_algo_feed">Hírfolyamok</string> - <string name="backup_entry_location">Helyek</string> - <string name="backup_entry_relay">Átjátszók</string> - <string name="backup_entry_relay_group">Csoportok</string> - <string name="backup_entry_chat_room">Csevegőszobák</string> - <string name="backup_entry_trust_provider">Bizalmi szolgáltatók</string> - <string name="backup_entry_mint">Pénzverdék</string> - <string name="backup_entry_nutzap_key">Nutzap kulcsok</string> - <string name="backup_entry_payment_target">Fizetési címek</string> - <string name="backup_entry_offer">Ajánlatok</string> - <string name="backup_entry_identity_claim">Kapcsolt identitások</string> - <string name="backup_entry_other">Egyéb bejegyzések</string> - <string name="backup_profile_field_name">Felhasználónév</string> - <string name="backup_profile_field_display_name">Megjelenített név</string> - <string name="backup_profile_field_about">Névjegy</string> - <string name="backup_profile_field_picture">Kép</string> - <string name="backup_profile_field_banner">Borító</string> - <string name="backup_profile_field_website">Weboldal</string> - <string name="backup_profile_field_nip05">Nostr-cím</string> - <string name="backup_profile_field_lud16">Lightning-cím</string> - <string name="backup_profile_field_lud06">LNURL</string> - <string name="backup_profile_field_pronouns">Névmások</string> - <string name="backup_profile_field_birthday">Születésnap</string> - <string name="backup_profile_field_clink_offer">CLINK-ajánlat</string> - <string name="backup_profile_field_bot">Bot jelölő</string> </resources> diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 601165a320..eda8c8fe1a 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -381,382 +381,4 @@ <string name="mute_button">Dźwięk włączony. Kliknij, aby wyciszyć</string> <string name="play">Odtwórz</string> <string name="pause">Pauza</string> - <string name="backup_conflict_title">Zmieniłeś %1$s w innej aplikacji</string> - <string name="backup_conflict_removed">Usunięto (%1$s)</string> - <string name="backup_conflict_added">Dodano (%1$s)</string> - <string name="backup_conflict_changed">Zmieniono (%1$s)</string> - <string name="backup_conflict_private_cleared">Usunąłeś wszystkie prywatne (zaszyfrowane) elementy.</string> - <string name="backup_conflict_private_changed">Zmieniłeś również elementy prywatne (zaszyfrowane). Nie można ich tutaj porównać.</string> - <string name="backup_conflict_private_added">Dodano prywatne (zaszyfrowane) pozycje.</string> - <string name="backup_conflict_restore_mine">Przywróć zapisaną wersję</string> - <string name="backup_review_saved">Na tym urządzeniu</string> - <string name="backup_review_new">Nowa wersja</string> - <string name="backup_review_not_set">Nie ustawiono</string> - <string name="backup_conflict_count_removed">Usunięto %1$s</string> - <string name="backup_conflict_count_added">Dodano %1$s</string> - <string name="backup_conflict_count_changed">%1$s zmieniono</string> - <plurals name="backup_card_follows_dropped"> - <item quantity="one">Przestałeś obserwować %1$d osobę</item> - <item quantity="few">Przestałeś obserwować %1$d osób</item> - <item quantity="many">Przestałeś obserwować %1$d osób</item> - <item quantity="other">Przestałeś obserwować %1$d osoby</item> - </plurals> - <string name="backup_card_accept">Zaakceptuj</string> - <string name="backup_review_title_updated">%1$s zaktualizowano</string> - <string name="backup_review_title_cleared">%1$s wyczyszczono</string> - <string name="backup_review_changed_by_other_app">Zmieniono w innej aplikacji · %1$s</string> - <string name="backup_action_keep_count">Zachowaj %1$s</string> - <string name="backup_action_restore_count">Przywróć %1$s</string> - <string name="backup_action_remute">Ponownie wycisz %1$s</string> - <string name="backup_action_rejoin">Ponów %1$s</string> - <string name="backup_action_restore_profile">Przywróć profil</string> - <string name="backup_action_restore_relays">Przywróć transmitery</string> - <string name="backup_review_kept_count">Zachowano %1$s</string> - <string name="backup_review_dropped_count">%1$s odrzuconych</string> - <string name="backup_review_new_count">%1$s nowych</string> - <string name="backup_review_tab_dropped">Usunięte · %1$s</string> - <string name="backup_review_tab_new">Nowe · %1$s</string> - <string name="backup_review_tab_edited">Edytowane · %1$s</string> - <string name="backup_review_search_people">Szukaj %1$s osób</string> - <string name="backup_review_nothing_here">Nic tu nie ma!</string> - <string name="backup_review_unmuted">%1$s odblokowanych</string> - <string name="backup_review_unmuted_explainer">Mogą one ponownie pojawić się w Twoich informacjach</string> - <string name="backup_review_newly_muted">%1$s nowo wyciszonych</string> - <string name="backup_review_words_and_hashtags">Wyrazy & hasztagi</string> - <string name="backup_review_names_and_more">%1$s i %2$s więcej</string> - <string name="backup_review_private_wiped_title">Usunąłeś wszystkie prywatne elementy</string> - <string name="backup_review_private_wiped_body">Są zaszyfrowane, więc tylko Ty możesz zobaczyć, co to było. Odtworzenie przywróci je wszystkie.</string> - <string name="backup_review_private_added_title">Ustawiłeś te pozycje jako prywatne</string> - <string name="backup_review_private_added_body">Są teraz zaszyfrowane, co prawdopodobnie wyjaśnia, gdzie podziały się powyższe elementy publiczne.</string> - <string name="backup_review_private_changed_title">Zmieniłeś prywatne elementy</string> - <string name="backup_review_private_changed_body">Są zaszyfrowane, więc nie można ich tu porównać.</string> - <string name="backup_review_gone">Zniknęła</string> - <string name="backup_review_new_tag">Nowa</string> - <string name="backup_review_dropped_tag">Usunięta</string> - <string name="backup_review_demoted_tag">Przeniesiona</string> - <string name="backup_review_units_tag">Jednostek</string> - <string name="backup_review_left_tag">Zostało</string> - <string name="backup_review_renamed_tag">Zmieniono nazwę</string> - <string name="backup_review_outbox">Skrzynka nadawcza</string> - <string name="backup_review_outbox_explainer">gdzie publikujesz</string> - <string name="backup_review_inbox">Skrzynka odbiorcza</string> - <string name="backup_review_inbox_explainer">gdzie inni docierają do Ciebie</string> - <string name="backup_review_key_replaced">Wymieniono klucz odbierający</string> - <string name="backup_review_key_removed">Usunięto klucz odbierający</string> - <string name="backup_review_key_warning">Nutzapy zostaną przypisane do tego nowego klucza. Jeśli urządzenie nie posiada tego klucza, może się okazać, że nie będzie można ich tutaj zrealizować.</string> - <string name="backup_review_left">zostało</string> - <string name="backup_review_joined">dołączył(a)</string> - <string name="backup_review_renamed">zmieniono nazwę</string> - <string name="backup_review_was_named">było „%1$s”</string> - <string name="backup_action_block_again">Zablokuj %1$s ponownie</string> - <string name="backup_action_restore_feeds">Przywróć kanały</string> - <string name="backup_action_restore_topics">Przywróć tematy</string> - <string name="backup_action_restore_places">Przywróć miejsca</string> - <string name="backup_action_restore_providers">Przywróć dostawców</string> - <string name="backup_action_restore_wallet">Przywróć portfel</string> - <string name="backup_review_kept">zachowane</string> - <string name="backup_review_receiving_tag">Pobieranie</string> - <string name="backup_review_listed_tag">Na Liście</string> - <string name="backup_review_searching_tag">Szukam</string> - <string name="backup_review_storing_tag">Przechowywanie</string> - <string name="backup_review_trusted_tag">Zaufane</string> - <string name="backup_review_untrusted_tag">Niezaufane</string> - <string name="backup_review_unblocked">%1$s odblokowane</string> - <string name="backup_review_unblocked_explainer">Ametyst ponownie połączy się z nimi</string> - <string name="backup_review_no_longer_blocked">Nie jest już zablokowana</string> - <string name="backup_review_newly_blocked">Nowo zablokowana</string> - <string name="backup_review_still_blocked">Nadal zablokowana</string> - <string name="backup_review_allowed_tag">Dozwolona</string> - <string name="backup_review_moved_tag">Przeniesiona</string> - <string name="backup_review_feed">Kanał</string> - <string name="backup_review_topics_gone">tematy zniknęły</string> - <string name="backup_review_places_gone"> miejsca zniknęły</string> - <string name="backup_review_new_count_caption">nowe</string> - <string name="backup_review_hashtags_you_follow">Hashtagi, które obserwujesz</string> - <string name="backup_review_places_you_follow">Miejsca które obserwujesz</string> - <string name="backup_review_services_changed">Zmieniono %1$s z %2$s serwisów</string> - <string name="backup_review_services_changed_explainer">Teraz pochodzą od kogoś innego albo od nikogo</string> - <string name="backup_review_service">Serwis</string> - <string name="backup_review_service_rank">Ranking</string> - <string name="backup_review_service_followers">Obserwujący</string> - <string name="backup_review_service_topics">Tematy</string> - <string name="backup_review_trust_warning">Rankingi, liczba obserwujących i filtrowanie spamu, które widzisz, pochodzą od tych dostawców.</string> - <plurals name="backup_review_payment_targets_gone"> - <item quantity="one">%1$d sposób płatności został usunięty</item> - <item quantity="few">%1$d sposobów płatności zostało usuniętych</item> - <item quantity="many">%1$d sposobów płatności zostało usuniętych</item> - <item quantity="other">%1$d sposoby płatności zostały usunięte</item> - </plurals> - <plurals name="backup_review_offers_gone"> - <item quantity="one">%1$d oferta usunięta</item> - <item quantity="few">%1$d ofert usunięto</item> - <item quantity="many">%1$d ofert usunięto</item> - <item quantity="other">%1$d oferty usunięte</item> - </plurals> - <string name="backup_review_payments_explainer">Osoby, które Ci płacą, widzą tylko to, co znajduje się na tej liście.</string> - <string name="backup_review_offer">Oferta BOLT12</string> - <string name="backup_review_encrypted_cleared">Opróżniłeś to</string> - <string name="backup_review_encrypted_added">Utworzyłeś to</string> - <string name="backup_review_encrypted_changed">Zmieniłeś to</string> - <string name="backup_review_wallet_body">Minty i klucze w Twoim portfelu są zaszyfrowane dla Ciebie, więc nie można ich tutaj porównać pozycja po pozycji. Przywrócenie danych powoduje przywrócenie dokładnie tego, co zostało zapisane na tym urządzeniu.</string> - <string name="backup_review_concord_body">Twoje społeczności w Concord są dla Ciebie zaszyfrowane, więc nie można ich tutaj porównać. Przywrócenie danych powoduje przywrócenie dokładnie tego, co zostało zapisane na tym urządzeniu.</string> - <plurals name="backup_card_mutes_visible"> - <item quantity="one">Włączyłeś dźwięk dla %1$d elementu</item> - <item quantity="few">Włączyłeś dźwięk dla %1$d elementów</item> - <item quantity="many">Włączyłeś dźwięk dla %1$d elementów</item> - <item quantity="other">%1$d elementy mają włączony dźwięk</item> - </plurals> - <plurals name="backup_card_profile_lost_fields"> - <item quantity="one">Usunąłeś %1$d pole profilu</item> - <item quantity="few">Usunąłeś %1$d pól profilowych</item> - <item quantity="many">Usunąłeś %1$d pól profilowych</item> - <item quantity="other">Usunąłeś %1$d pola profilowe</item> - </plurals> - <plurals name="backup_card_nip65_lost"> - <item quantity="one">Usunąłeś %1$d publiczny transmiter</item> - <item quantity="few">Usunąłeś %1$d publicznych transmiterów</item> - <item quantity="many">Usunąłeś %1$d publicznych transmiterów</item> - <item quantity="other">Usunąłeś %1$d publiczne transmitery</item> - </plurals> - <plurals name="backup_card_dm_relays_lost"> - <item quantity="one">Usunąłeś %1$d transmiter DM</item> - <item quantity="few">Usunąłeś %1$d transmiterów DM</item> - <item quantity="many">Usunąłeś %1$d transmiterów DM</item> - <item quantity="other">Usunąłeś %1$d transmitery DM</item> - </plurals> - <plurals name="backup_card_key_package_relays_lost"> - <item quantity="one">Usunięto %1$d transmiter klucza grupowego czatu</item> - <item quantity="few">Usunięto %1$d transmiterów klucza grupowego czatu</item> - <item quantity="many">Usunięto %1$d transmiterów klucza grupowego czatu</item> - <item quantity="other">Usunięto %1$d transmitery klucza grupowego czatu</item> - </plurals> - <plurals name="backup_card_search_relays_lost"> - <item quantity="one">Usunięto %1$d transmiter wyszukiwania</item> - <item quantity="few">Usunięto %1$d transmiterów wyszukiwania</item> - <item quantity="many">Usunięto %1$d transmiterów wyszukiwania</item> - <item quantity="other">Usunięto %1$d transmitery wyszukiwania</item> - </plurals> - <plurals name="backup_card_indexer_relays_lost"> - <item quantity="one">Usunięto %1$d transmiter indeksujący</item> - <item quantity="few">Usunięto %1$d transmiterów indeksujących</item> - <item quantity="many">Usunięto %1$d transmiterów indeksujących</item> - <item quantity="other">Usunięto %1$d transmitery indeksujące</item> - </plurals> - <plurals name="backup_card_relay_feeds_lost"> - <item quantity="one">Usunięto %1$d kanał transmitera</item> - <item quantity="few">Usunięto %1$d kanałów transmitera</item> - <item quantity="many">Usunięto %1$d kanałów transmitera</item> - <item quantity="other">Usunięto %1$d kanały transmitera</item> - </plurals> - <plurals name="backup_card_private_outbox_lost"> - <item quantity="one">Usunięto %1$d prywatny transmiter</item> - <item quantity="few">Usunięto %1$d prywatnych transmiterów</item> - <item quantity="many">Usunięto %1$d prywatnych transmiterów</item> - <item quantity="other">Usunięto %1$d prywatne transmitery</item> - </plurals> - <plurals name="backup_card_trusted_lost"> - <item quantity="one">Przestałeś ufać %1$d transmiterowi</item> - <item quantity="few">Przestałeś ufać %1$d transmiterom</item> - <item quantity="many">Przestałeś ufać %1$d transmiterom</item> - <item quantity="other">Przestałeś ufać %1$d transmiterom</item> - </plurals> - <plurals name="backup_card_unblocked"> - <item quantity="one">Odblokowałeś %1$d transmiter</item> - <item quantity="few">Odblokowałeś %1$d transmiterów</item> - <item quantity="many">Odblokowałeś %1$d transmiterów</item> - <item quantity="other">Odblokowałeś %1$d transmitery</item> - </plurals> - <plurals name="backup_card_leave_chats"> - <item quantity="one">Opuściłeś %1$d czat publiczny</item> - <item quantity="few">Opuściłeś %1$d czatów publicznych</item> - <item quantity="many">Opuściłeś %1$d czatów publicznych</item> - <item quantity="other">Opuściłeś %1$d czaty publiczne</item> - </plurals> - <plurals name="backup_card_leave_communities"> - <item quantity="one">Opuściłeś %1$d społeczność</item> - <item quantity="few">Opuściłeś %1$d społeczności</item> - <item quantity="many">Opuściłeś %1$d społeczności</item> - <item quantity="other">Opuściłeś %1$d społeczności</item> - </plurals> - <plurals name="backup_card_leave_rooms"> - <item quantity="one">Opuściłeś %1$d pokój czatu</item> - <item quantity="few">Opuściłeś %1$d pokoi czatu</item> - <item quantity="many">Opuściłeś %1$d pokoi czatu</item> - <item quantity="other">Opuściłeś %1$d pokoje czatu</item> - </plurals> - <plurals name="backup_card_leave_groups"> - <item quantity="one">Opuściłeś %1$d grupę</item> - <item quantity="few">Opuściłeś %1$d grup</item> - <item quantity="many">Opuściłeś %1$d grup</item> - <item quantity="other">Opuszczono %1$d grupy</item> - </plurals> - <plurals name="backup_card_feeds_removed"> - <item quantity="one">Usunąłeś %1$d ulubiony kanał</item> - <item quantity="few">Usunąłeś %1$d ulubionych kanałów</item> - <item quantity="many">Usunąłeś %1$d ulubionych kanałów</item> - <item quantity="other">Usunięto %1$d ulubione kanały</item> - </plurals> - <plurals name="backup_card_hashtags_unfollowed"> - <item quantity="one">Przestałeś obserwować %1$d hashtag</item> - <item quantity="few">Przestałeś obserwować %1$d hashtagów</item> - <item quantity="many">Przestałeś obserwować %1$d hashtagów</item> - <item quantity="other">Przestałeś obserwować %1$d hashtagi</item> - </plurals> - <plurals name="backup_card_places_unfollowed"> - <item quantity="one">Przestałeś obserwować %1$d miejsce</item> - <item quantity="few">Przestałeś obserwować %1$d miejsc</item> - <item quantity="many">Przestałeś obserwować %1$d miejsc</item> - <item quantity="other">Przestałeś obserwować %1$d miejsca</item> - </plurals> - <plurals name="backup_card_trust_services_changed"> - <item quantity="one">Zmieniłeś dostawcę %1$d zaufanego serwisu</item> - <item quantity="few">Zmieniłeś dostawcę %1$d zaufanych serwisów</item> - <item quantity="many">Zmieniłeś dostawcę %1$d zaufanych serwisów</item> - <item quantity="other">Zmieniłeś dostawcę %1$d zaufanych serwisów</item> - </plurals> - <plurals name="backup_card_nutzap_mints_removed"> - <item quantity="one">Usunięto %1$d nutzap mint</item> - <item quantity="few">Usunięto %1$d nutzap mintów</item> - <item quantity="many">Usunięto %1$d nutzap mintów</item> - <item quantity="other">Usunięto %1$d nutzap minty</item> - </plurals> - <plurals name="backup_card_payment_targets_removed"> - <item quantity="one">Usunięto %1$d sposób na otrzymanie zapłaty</item> - <item quantity="few">Usunięto %1$d sposobów otrzymywania zapłaty</item> - <item quantity="many">Usunięto %1$d sposobów otrzymywania zapłaty</item> - <item quantity="other">Usunięto %1$d sposoby otrzymywania zapłaty</item> - </plurals> - <plurals name="backup_card_offers_removed"> - <item quantity="one">Usunięto %1$d ofertę BOLT12</item> - <item quantity="few">Usunięto %1$d ofert BOLT12</item> - <item quantity="many">Usunięto %1$d ofert BOLT12</item> - <item quantity="other">Usunięto %1$d oferty BOLT12</item> - </plurals> - <plurals name="backup_card_and_added"> - <item quantity="one">%1$s i dodano %2$d</item> - <item quantity="few">%1$s i dodano %2$d</item> - <item quantity="many">%1$s i dodano %2$d</item> - <item quantity="other">%1$s i dodano %2$d</item> - </plurals> - <plurals name="backup_card_and_followed"> - <item quantity="one">%1$s i obserwowany %2$d</item> - <item quantity="few">%1$s i obserwowanych %2$d</item> - <item quantity="many">%1$s i obserwowanych %2$d</item> - <item quantity="other">%1$s i obserwowane %2$d</item> - </plurals> - <plurals name="backup_card_and_muted"> - <item quantity="one">%1$s i wyciszony %2$d</item> - <item quantity="few">%1$s i wyciszono %2$d</item> - <item quantity="many">%1$s i wyciszono %2$d</item> - <item quantity="other">%1$s i wyciszone %2$d</item> - </plurals> - <plurals name="backup_card_and_joined"> - <item quantity="one">%1$s i dołączono do %2$d</item> - <item quantity="few">%1$s i dołączono do %2$d</item> - <item quantity="many">%1$s i dołączono do %2$d</item> - <item quantity="other">%1$s i dołączono do %2$d</item> - </plurals> - <plurals name="backup_card_and_blocked"> - <item quantity="one">%1$s i zablokowano %2$d</item> - <item quantity="few">%1$s i zablokowano %2$d</item> - <item quantity="many">%1$s i zablokowano %2$d</item> - <item quantity="other">%1$s i zablokowano %2$d</item> - </plurals> - <string name="backup_card_app_settings">Twoje zsynchronizowane ustawienia aplikacji Amethyst zostały zmienione w innej aplikacji, która nie jest Amethystem. To może powodować problemy.</string> - <string name="backup_card_nutzap_key_replaced">Wymieniłeś swój klucz Nutzap</string> - <string name="backup_card_nutzap_key_removed">Usunąłeś swój klucz nutzap</string> - <string name="backup_card_wallet_emptied">Opróżniłeś swój portfel Cashu</string> - <string name="backup_card_wallet_rewritten">Zmieniłeś swój portfel Cashu</string> - <string name="backup_card_concord_emptied">Opróżniłeś swoją listę Concorda</string> - <string name="backup_card_concord_rewritten">Zmieniłeś swoją listę Concorda</string> - <string name="backup_conflict_keep_new">Użyj nowej wersji</string> - <string name="backup_conflict_relay_read_only">tylko do odczytu</string> - <string name="backup_conflict_relay_write_only">tylko do zapisu</string> - <string name="backup_conflict_relay_read_write">odczyt i zapis</string> - <string name="backup_type_profile">profil</string> - <string name="backup_type_profile_explainer">Twoje imię i nazwisko, zdjęcie, biografia oraz adresy do płatności – tak, jak widzą je wszyscy.</string> - <string name="backup_type_follow_list">lista obserwowanych</string> - <string name="backup_type_follow_list_explainer">Określa, czyje posty pojawiają się w Twoim kanale głównym.</string> - <string name="backup_type_mute_list">lista zablokowanych</string> - <string name="backup_type_mute_list_explainer">Ukrywa osoby, słowa, hashtagi i wątki we wszystkich wyświetlanych treściach.</string> - <string name="backup_type_outbox_inbox_relays">lista publicznych transmiterów</string> - <string name="backup_type_outbox_inbox_relays_explainer">Informuje wszystkich, gdzie mogą znaleźć Twoje posty oraz gdzie mają wysyłać Ci odpowiedzi i wzmianki.</string> - <string name="backup_type_dm_relays">Lista transmiterów DM</string> - <string name="backup_type_dm_relays_explainer">Gdzie inni dostarczają Twoje prywatne wiadomości.</string> - <string name="backup_type_key_package_relays">transmitery kluczy czatu grupowego</string> - <string name="backup_type_key_package_relays_explainer">Gdzie inni znajdą klucze do dodawania cię do zaszyfrowanych czatów grupowych.</string> - <string name="backup_type_search_relays">lista transmiterów wyszukiwania</string> - <string name="backup_type_search_relays_explainer">Transmitery używane podczas wyszukiwania.</string> - <string name="backup_type_indexer_relays">lista transmiterów indeksujących</string> - <string name="backup_type_indexer_relays_explainer">Transmitery służące do wyszukiwania profili innych osób oraz list transmiterów.</string> - <string name="backup_type_relay_feeds">ulubione kanały transmitera</string> - <string name="backup_type_relay_feeds_explainer">Transmitery, które przeglądasz jako kanały informacyjne.</string> - <string name="backup_type_blocked_relays">lista zablokowanych przekaźników</string> - <string name="backup_type_blocked_relays_explainer">Transmitery, z którymi ta aplikacja nigdy się nie łączy.</string> - <string name="backup_type_trusted_relays">lista zaufanych transmiterów</string> - <string name="backup_type_trusted_relays_explainer">Zaufane transmitery, z którymi bez wahania nawiązujesz połączenie.</string> - <string name="backup_type_private_outbox_relays">lista prywatnych transmiterów</string> - <string name="backup_type_private_outbox_relays_explainer">Gdzie przechowywane są Twoje projekty i prywatne dane.</string> - <string name="backup_type_app_settings">zsynchronizowane ustawienia aplikacji</string> - <string name="backup_type_app_settings_explainer">Ustawienia Amethyst są synchronizowane na wszystkich Twoich urządzeniach. Są one zaszyfrowane, więc można porównać jedynie sam fakt ich obecności.</string> - <string name="backup_type_public_chats">lista czatów publicznych</string> - <string name="backup_type_public_chats_explainer">Publiczne czaty, do których dołączyłeś.</string> - <string name="backup_type_communities">lista społeczności</string> - <string name="backup_type_communities_explainer">Społeczności, do których dołączyłeś.</string> - <string name="backup_type_hashtags">obserwowane hashtagi</string> - <string name="backup_type_hashtags_explainer">Hashtagi, z których pochodzą posty wyświetlane w Twoich kanałach.</string> - <string name="backup_type_geohashes">obserwowane lokalizacje</string> - <string name="backup_type_geohashes_explainer">Miejsca, z których posty pojawiają się na Twoich kanałach.</string> - <string name="backup_type_favorite_algo_feeds">lista ulubionych kanałów</string> - <string name="backup_type_favorite_algo_feeds_explainer">Zapisane przez Ciebie kanały algorytmiczne.</string> - <string name="backup_type_ephemeral_chats">lista czatów efemerycznych</string> - <string name="backup_type_ephemeral_chats_explainer">Tymczasowe czaty, do których dołączyłeś.</string> - <string name="backup_type_relay_groups">lista grup</string> - <string name="backup_type_relay_groups_explainer">Grupy oparte na transmiterach, do których dołączyłeś.</string> - <string name="backup_type_concord_communities">Lista społeczności Concord</string> - <string name="backup_type_concord_communities_explainer">Społeczności Concord, do których dołączyłeś. Są one zaszyfrowane, więc można porównać jedynie fakt ich istnienia.</string> - <string name="backup_type_trust_providers">lista zaufanych dostawców</string> - <string name="backup_type_trust_providers_explainer">Serwisy, którym ufasz, a które zajmują się klasyfikowaniem i weryfikacją osób w Twoim imieniu.</string> - <string name="backup_type_cashu_wallet">Portfel Cashu</string> - <string name="backup_type_cashu_wallet_explainer">Ustawienia portfela ecash oraz minty. Są one zaszyfrowane, więc można porównać jedynie sam fakt ich istnienia.</string> - <string name="backup_type_nutzap_info">ustawienia odbioru nutzap</string> - <string name="backup_type_nutzap_info_explainer">Minty i klucz, które inni używają do wysyłania ci „nutzapów”.</string> - <string name="backup_type_payment_targets">adresy płatności</string> - <string name="backup_type_payment_targets_explainer">Sposoby, w jakie inni mogą Ci zapłacić.</string> - <string name="backup_type_bolt12_offers">Oferta BOLT12</string> - <string name="backup_type_bolt12_offers_explainer">Ta możliwość pozwala innym płacić Ci za pomocą Lightning.</string> - <string name="backup_type_other">lista</string> - <string name="backup_type_other_explainer">Lista zapisana na tym urządzeniu.</string> - <string name="backup_entry_profile_field">Pola profilu</string> - <string name="backup_entry_person">Osoby</string> - <string name="backup_entry_hashtag">Hashtagi</string> - <string name="backup_entry_word">Słowa</string> - <string name="backup_entry_thread">Wątki</string> - <string name="backup_entry_public_chat">Czaty publiczne</string> - <string name="backup_entry_community">Społeczności</string> - <string name="backup_entry_algo_feed">Kanały</string> - <string name="backup_entry_location">Lokalizacje</string> - <string name="backup_entry_relay">Transmitery</string> - <string name="backup_entry_relay_group">Grupy</string> - <string name="backup_entry_chat_room">Czat pokoje</string> - <string name="backup_entry_trust_provider">Zaufani dostawcy</string> - <string name="backup_entry_mint">Minty</string> - <string name="backup_entry_nutzap_key">Klucze Nutzap</string> - <string name="backup_entry_payment_target">Adresy płatności</string> - <string name="backup_entry_offer">Oferty</string> - <string name="backup_entry_identity_claim">Powiązane tożsamości</string> - <string name="backup_entry_other">Inne wpisy</string> - <string name="backup_profile_field_name">Nazwa użytkownika</string> - <string name="backup_profile_field_display_name">Nazwa wyświetlana</string> - <string name="backup_profile_field_about">O profilu</string> - <string name="backup_profile_field_picture">Zdjęcie</string> - <string name="backup_profile_field_banner">Baner</string> - <string name="backup_profile_field_website">Strona www</string> - <string name="backup_profile_field_nip05">Adres Nostr</string> - <string name="backup_profile_field_lud16">Lightning Adres</string> - <string name="backup_profile_field_lud06">LNURL</string> - <string name="backup_profile_field_pronouns">Zaimki</string> - <string name="backup_profile_field_birthday">Data urodzin</string> - <string name="backup_profile_field_clink_offer">Oferta CLINK</string> - <string name="backup_profile_field_bot">Flaga bota</string> </resources> diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 2da4657d8d..3803e00f8a 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -305,250 +305,6 @@ - <string name="cordn_groups_manage">Coordinators</string> - <string name="cordn_groups_down_note">Nothing is arriving from these groups. Amethyst keeps retrying.</string> - <string name="cordn_groups_none">No cordn groups yet</string> - <string name="cordn_groups_none_detail">A cordn group lives on one coordinator that holds its membership and passes messages along, without ever seeing what anyone writes.</string> - <string name="cordn_groups_start">Start one</string> - <string name="cordn_group_unavailable">This group is not available. Its coordinator may have been removed.</string> - <string name="cordn_group_info">Group info</string> - <string name="cordn_message_deleted">Message deleted</string> - <string name="cordn_message_edited">edited</string> - <string name="cordn_action_go_to_message">Go to message</string> - <string name="cordn_delete_confirm_title">Withdraw message</string> - <string name="cordn_delete_confirm_body">Everyone in the group will see this message as withdrawn, and Amethyst will stop showing its text. The coordinator still holds the encrypted copy it was sent as, and anyone who already read it has already read it.</string> - <string name="cordn_delivery_accepted">Accepted by the coordinator</string> - <string name="cordn_message_details_title">Message details</string> - <string name="cordn_message_details_sent_at">Sent</string> - <string name="cordn_message_details_cursor">Cursor</string> - <string name="cordn_message_details_coordinator">Coordinator</string> - <string name="cordn_composer_hint">Message</string> - <string name="cordn_action_reply">Reply</string> - <string name="cordn_action_edit">Edit</string> - <string name="cordn_action_delete">Delete</string> - <string name="cordn_action_pin">Pin</string> - <string name="cordn_pinned_previous">Previous pinned message</string> - <string name="cordn_pinned_next">Next pinned message</string> - <string name="cordn_pinned_show_all">Show all pinned messages</string> - <string name="cordn_pinned_title">Pinned messages</string> - <string name="cordn_pinned_by">Pinned by %1$s</string> - <plurals name="cordn_pinned_count"> - <item quantity="one">%1$d pinned message in this group</item> - <item quantity="other">%1$d pinned messages in this group</item> - </plurals> - <string name="cordn_action_unpin">Unpin</string> - <string name="cordn_action_editing">Editing your message</string> - <string name="cordn_media_upload_failed">Could not send that file.</string> - <string name="cordn_send_failed">That did not send.</string> - <string name="cordn_send_no_session">This group\'s coordinator is not open, so nothing can be sent yet.</string> - <string name="cordn_media_download_failed">Could not open that file.</string> - <string name="cordn_media_no_server">No media server is set for this account, so there is nowhere to put the file. Pick one in Settings.</string> - <string name="cordn_media_unreadable">That file could not be read.</string> - <string name="cordn_voice_record">Record a voice note</string> - <string name="cordn_voice_stop">Stop and send</string> - <string name="cordn_voice_play">Play voice note</string> - <string name="cordn_backup_explainer">Your nsec cannot bring cordn groups back. A cordn group lives as encryption state on this device and an ordered stream on a coordinator that cannot read it — lose the device without a backup and the group is gone, including everything already said in it.</string> - <string name="cordn_backup_passphrase">Passphrase</string> - <string name="cordn_backup_contents_title">What is in the file</string> - <string name="cordn_backup_contents_body">Your group encryption keys, your key packages, and every message in those groups. Anyone who opens it can read the conversations. The passphrase is the only thing protecting it once it leaves Amethyst.</string> - <string name="cordn_backup_export">Export</string> - <string name="cordn_backup_exported">Backup saved.</string> - <string name="cordn_backup_restore_title">Restore</string> - <string name="cordn_backup_restore_body">Restoring replaces the cordn groups on this device with the ones in the file. It is for a phone that has taken over from another — not a way to use the same groups on two devices at once.</string> - <string name="cordn_backup_restore">Restore</string> - <string name="cordn_backup_restore_confirm_title">Replace this device\'s cordn groups?</string> - <string name="cordn_backup_restore_confirm_body">Every cordn group now on this device will be removed and replaced by the ones in the file.\n\nDo not restore onto a second phone while the first is still in use. Both would hold the same group keys, and once both have sent a message the group breaks for everyone — silently, and with no way back.</string> - <string name="cordn_backup_restored">Restored.</string> - <string name="cordn_backup_failed">That did not work. Check the passphrase and the file.</string> - <string name="cordn_send">Send</string> - <string name="cordn_info_coordinator">Coordinator</string> - <string name="cordn_info_coordinator_key">Coordinator key</string> - <string name="cordn_info_coordinator_nprofile">Coordinator link</string> - <string name="cordn_info_copy_nprofile">Copy the coordinator\'s nprofile</string> - <string name="cordn_info_gid">Group id</string> - <string name="cordn_info_epoch">Epoch</string> - <string name="cordn_info_members">Members</string> - <string name="cordn_info_egalitarian">This group has no admins: everyone can add, remove and rename, permanently.</string> - <string name="cordn_info_technical">Technical details</string> - <string name="cordn_create_title">New Cordn group</string> - <string name="cordn_create_explainer">A cordn group is ordered by one coordinator you choose. Creating the group happens on this device — the coordinator only learns the group exists once you invite someone or send a message.</string> - <string name="cordn_create_people_none">Nobody yet — tap to add people</string> - <string name="cordn_create_people_empty">Nobody added yet. A group can be created empty and invitations sent later, but naming people now is what shows which coordinator can reach them.</string> - <string name="cordn_create_people_done">Done</string> - <string name="cordn_create_you">You, always an admin</string> - <string name="cordn_create_step_name">Name it</string> - <string name="cordn_create_step_people">Who is in it</string> - <string name="cordn_create_step_where">Where it lives</string> - <string name="cordn_create_step_admins">Who can add and remove</string> - <string name="cordn_create_add_member">Add someone</string> - <string name="cordn_create_people_note">Naming people now is what tells us which coordinator can reach them. Each one gets a Welcome left on that coordinator, which they pick up next time they open their app.</string> - <string name="cordn_create_reach_unknown">Not checked yet</string> - <string name="cordn_create_reach_none">No key on any coordinator you use</string> - <string name="cordn_create_egalitarian">Everyone can add and remove</string> - <string name="cordn_create_admin">Admin</string> - <string name="cordn_create_no_coordinator">No coordinator chosen</string> - <string name="cordn_create_coordinator_change">Change</string> - <string name="cordn_create_coordinator_hide">Done</string> - <string name="cordn_create_coverage_unknown">Not checked — picking it will ask</string> - <string name="cordn_create_coverage_all">Reaches all %1$d of your people</string> - <string name="cordn_create_coverage_partial">Reaches %1$d of %2$d — the rest would be left out</string> - <string name="cordn_create_outcome_title">The group exists</string> - <string name="cordn_create_outcome_explainer">Each invitation is a Welcome left on the coordinator, so they can fail one at a time. Nobody has been notified: they see it next time they open their app.</string> - <string name="cordn_create_outcome_no_welcome">Added to the group, but the invitation did not reach them — try again from the group screen</string> - <string name="cordn_create_outcome_waiting">Waiting for them</string> - <string name="cordn_create_outcome_failed">The coordinator refused the invitation</string> - <string name="cordn_create_outcome_skipped">No key here, so no Welcome could be left</string> - <string name="cordn_create_outcome_link">Send a link</string> - <string name="cordn_create_outcome_open">Open the chat</string> - <string name="cordn_create_coordinator">Coordinator</string> - <string name="cordn_create_coordinator_discover">Find coordinators</string> - <string name="cordn_create_coordinator_new">Another coordinator</string> - <string name="cordn_create_coordinator_pubkey">Coordinator public key (hex or npub)</string> - <string name="cordn_create_coordinator_relays">Relays it answers on, one per line</string> - <string name="cordn_create_name">Group name</string> - <string name="cordn_create_description">Description (optional)</string> - <string name="cordn_create_egalitarian_note">Everyone in the group can invite, remove members and change its details. Any member can name admins later, which ends that.</string> - <string name="cordn_create_admin_only_me">Only I manage this group</string> - <string name="cordn_admin_action_failed">That change could not be made.</string> - <string name="cordn_info_remove_member">Remove</string> - <string name="cordn_info_remove_confirm_title">Remove from group?</string> - <string name="cordn_info_remove_confirm_body">%1$s stops receiving messages from here. They keep what they have already read, and this cannot be undone — they would have to be invited again.</string> - <string name="cordn_info_edit_details">Edit details</string> - <string name="cordn_info_save">Save</string> - <string name="cordn_create_admin_only_me_on">Only you can invite, remove members or change the group\'s details. You can hand that to someone else later.</string> - <string name="cordn_create_action">Create group</string> - <string name="cordn_create_failed">The group could not be created.</string> - <string name="cordn_invitations_title">Cordn invitations</string> - <string name="cordn_invitations_explainer">Checked when you open this screen, and at no other time — asking a coordinator whether anyone invited you tells it you are here, so Amethyst does not do it in the background.</string> - <string name="cordn_invitations_refresh">Check again</string> - <string name="cordn_invitations_accept">Join</string> - <string name="cordn_invitations_decline">Decline</string> - <string name="cordn_invitations_decline_warning">Declining is permanent. Getting back in means being invited again.</string> - <string name="cordn_chat_yesterday">Yesterday</string> - <string name="cordn_chat_unread_divider">New messages</string> - <string name="cordn_chat_empty_title">No messages yet</string> - <string name="cordn_chat_empty_description">Anything sent here is encrypted for this group. The coordinator relays it without being able to read it.</string> - <string name="cordn_reactions_title">Reactions</string> - <string name="cordn_group_unavailable_detail">Add a coordinator in Settings to start using cordn groups.</string> - <string name="cordn_coordinators_discover_explainer">Reads announcements your relays already carry. No coordinator is contacted, so none of them learns you looked.</string> - <string name="cordn_coordinators_discover_action">Look for coordinators</string> - <string name="cordn_coordinators_discover_add">Add</string> - <string name="cordn_coordinators_discover_failed">Could not read announcements</string> - <string name="cordn_coordinators_discover_none">Nobody is announcing on your relays.</string> - <string name="cordn_coordinators_discover_unheard">Nothing found, and %1$d of your relays did not answer.</string> - <string name="cordn_coordinators_discover_seen">Announced %1$s ago</string> - <string name="cordn_coordinators_discover_seen_on">Last announced %1$s</string> - <string name="cordn_coordinators_relays">%1$s</string> - <string name="cordn_coordinators_relays_more">%1$s +%2$d more</string> - <string name="cordn_coordinators_show_all">Show all %1$d</string> - <string name="cordn_coordinators_show_fewer">Show fewer</string> - <string name="cordn_coordinators_show_older">Show %1$d that stopped announcing</string> - <string name="cordn_coordinators_hide_older">Hide the ones that stopped announcing</string> - <string name="cordn_coordinators_stale_note">These have not announced in over a month. Creating a group on one that has gone away will fail.</string> - <string name="cordn_info_add_member">Add someone</string> - <string name="cordn_info_add_member_placeholder">Name, npub or name@domain</string> - <string name="cordn_info_has_key_package">Can be added on this coordinator</string> - <string name="cordn_info_add_member_note">They can only be added if they published a key package to this coordinator.</string> - <string name="cordn_exposure_open">What this coordinator can see</string> - <string name="cordn_exposure_close">Got it</string> - <string name="cordn_admin_someone">That person</string> - <string name="cordn_admin_no_key_package">%1$s has not published a key package to this coordinator, so they cannot be added yet. Ask them to open cordn on this coordinator first.</string> - <string name="cordn_admin_wrong_key_package">This coordinator served a key package that belongs to someone else, so %1$s was not added. Nothing was changed.</string> - <string name="cordn_admin_no_welcome">%1$s could not be given a way into the group, so the add was abandoned.</string> - <string name="cordn_admin_not_a_member">%1$s is not in this group.</string> - <string name="cordn_admin_no_self_remove">You cannot remove yourself from a cordn group.</string> - <string name="cordn_admin_coordinator_silent">The coordinator did not answer, so nothing was changed. Try again in a moment.</string> - <string name="cordn_info_add_member_none">Nobody found by that name.</string> - <string name="cordn_info_admin_badge">Admin</string> - <string name="cordn_invitations_members_more">+%1$d more</string> - <string name="cordn_invitations_via">Through %1$s</string> - <string name="cordn_invitations_none">No invitations waiting.</string> - <string name="cordn_invitations_no_coordinators">You have no coordinator open. cordn invitations arrive through a coordinator, so there is nowhere to look yet.</string> - <string name="cordn_invitations_unreachable">%1$s did not answer</string> - <string name="cordn_invitations_skipped">An invitation was left for another device</string> - <string name="cordn_invitations_load_failed">Could not read invitations.</string> - <string name="cordn_link_request">Ask to join this group</string> - <string name="cordn_link_request_disclosure">This publishes a key package under your own account key and tells the coordinator you want into this group — whether or not anyone answers.</string> - <string name="cordn_link_request_sent">Asked. A member of the group has to add you; the invitation will show up under cordn invitations.</string> - <string name="cordn_link_request_failed">Could not ask to join.</string> - <string name="cordn_requests_title">Requests to join</string> - <string name="cordn_requests_check">Check for requests</string> - <string name="cordn_requests_none">Nobody is waiting to join.</string> - <string name="cordn_requests_accept">Add</string> - <string name="cordn_requests_decline">Dismiss</string> - <string name="cordn_requests_admin_only">Only an admin can accept. In a group with no admins, that is everyone.</string> - <string name="cordn_requests_failed">Could not read the requests.</string> - <string name="cordn_link_scan">Scan</string> - <string name="cordn_share_title">Share this group</string> - <string name="cordn_share_explainer">Anyone with this link can ask to join. An admin still has to let them in.</string> - <string name="cordn_share_copy">Copy link</string> - <string name="cordn_share_copied">Copied</string> - <string name="cordn_coordinators_explainer">A coordinator orders one group\'s messages and is the only thing that can. Losing it loses the ordering, and a second one does not mirror the first — so these are named things you choose, not interchangeable relays.</string> - <string name="cordn_coordinators_none">No coordinators yet.</string> - <string name="cordn_coordinators_add_action">Add</string> - <string name="cordn_coordinators_add_failed">Could not open that coordinator.</string> - <string name="cordn_coordinators_label">A name for it (yours, optional)</string> - <string name="cordn_coordinators_relays_label">Answers on</string> - <string name="cordn_coordinators_key">Coordinator key</string> - <string name="cordn_coordinators_copy_key">Copy the coordinator\'s npub</string> - <string name="cordn_coordinators_more">More actions</string> - <string name="cordn_coordinators_rename">Rename</string> - <string name="cordn_coordinators_label_note">This name is yours and stays on this device. A coordinator cannot prove a name, so it is never asked for one.</string> - <string name="cordn_coordinators_identify">Ask who it is</string> - <string name="cordn_coordinators_server">Says it is: %1$s</string> - <string name="cordn_coordinators_server_silent">Answered, but named nothing.</string> - <string name="cordn_coordinators_server_claim">A coordinator\'s name and version are strings it chose. Its public key is the only thing that identifies it.</string> - <string name="cordn_coordinators_health_ok">Answering.</string> - <string name="cordn_coordinators_health_retrying">A call failed; retrying.</string> - <string name="cordn_coordinators_health_down">Not answering — %1$d calls in a row have failed. Groups on it are not syncing.</string> - <string name="cordn_coordinators_health_unknown">Nothing asked of it yet.</string> - <string name="cordn_coordinators_remove">Remove</string> - <string name="cordn_coordinators_purge">Purge</string> - <string name="cordn_coordinators_purge_title">Purge this coordinator?</string> - <string name="cordn_coordinators_purge_body">Removing keeps the groups on this device, so adding the coordinator back brings them with it. Purging deletes them: the group keys, the read positions and your key packages all go, and those groups can only be re-entered by a fresh invitation.\n\nThe coordinator is not told and keeps whatever it already had.</string> - <string name="cordn_keypackages_explainer">Others can only add you to a cordn group by taking a key package you published to that coordinator. cordn has no other place to keep one, so with none published nobody can invite you — and nothing tells them why.</string> - <plurals name="cordn_create_unreachable_count"> - <item quantity="one">%1$d person cannot be reached by any coordinator you use</item> - <item quantity="other">%1$d people cannot be reached by any coordinator you use</item> - </plurals> - <plurals name="cordn_groups_count"> - <item quantity="one">%1$d group</item> - <item quantity="other">%1$d groups</item> - </plurals> - <plurals name="cordn_create_reach_count"> - <item quantity="one">Reachable on %1$d coordinator</item> - <item quantity="other">Reachable on %1$d coordinators</item> - </plurals> - <plurals name="cordn_create_admin_count"> - <item quantity="one">%1$d person can add and remove</item> - <item quantity="other">%1$d people can add and remove</item> - </plurals> - <plurals name="cordn_create_action_invite"> - <item quantity="one">Create and invite %1$d person</item> - <item quantity="other">Create and invite %1$d people</item> - </plurals> - <plurals name="cordn_member_count"> - <item quantity="one">%1$d member</item> - <item quantity="other">%1$d members</item> - </plurals> - <plurals name="cordn_keypackages_summary"> - <item quantity="one">%1$d single-use package available.</item> - <item quantity="other">%1$d single-use packages available.</item> - </plurals> - <string name="cordn_keypackages_last_resort_yes">A last-resort package is published, so an invitation can still be made once the single-use ones run out.</string> - <string name="cordn_keypackages_last_resort_no">No last-resort package. Once the single-use ones run out, invitations fail.</string> - <plurals name="cordn_keypackages_orphans"> - <item quantity="one">%1$d package this device cannot open</item> - <item quantity="other">%1$d packages this device cannot open</item> - </plurals> - <string name="cordn_keypackages_orphans_body">The coordinator will hand these to anyone inviting you, but the private half is not on this device — it belongs to another install. If that install is gone, an invitation using one of these produces a welcome nobody can ever open.</string> - <string name="cordn_keypackages_withdraw_orphans">Withdraw those</string> - <string name="cordn_keypackages_disclosure">Publishing signs a record under your own account key. That coordinator then knows this account exists and is invitable — permanently, and whether or not anyone invites you.</string> - <string name="cordn_keypackages_publish">Publish one</string> - <string name="cordn_keypackages_publish_last_resort">Publish last-resort</string> - <string name="cordn_keypackages_withdraw_all">Withdraw all</string> - <string name="cordn_keypackages_topup_note">Once you have published here, Amethyst quietly replaces packages as they are used up. It never publishes the first one for you.</string> - <string name="cordn_keypackages_failed">Could not read the key packages.</string> <!-- Buzz calls its groups channels, and a Buzz relay is one workspace rather than a directory of groups. --> <string name="buzz_channel_flag_forum">Forum channel</string> <string name="buzz_channel_flag_forum_desc">Threaded posts instead of a chat timeline. This cannot be changed later.</string> @@ -899,10 +655,6 @@ <!-- My Fitness: the signed-in user's own training summary. Android resource because NavBarItemDef labels are R.string ids. --> - <plurals name="cordn_invitations_members"> - <item quantity="one">%1$d person is already in this group</item> - <item quantity="other">%1$d people are already in this group</item> - </plurals> <string name="private_message">Private Message</string> <string name="pow_kind_chat_message">Chat message</string> <string name="nest_notification_broadcasting">Nest — Live</string> @@ -931,326 +683,4 @@ <string name="mute_button">Sound on. Click to mute</string> <string name="play">Play</string> <string name="pause">Pause</string> - <string name="backup_conflict_title">You changed your %1$s in another app</string> - <string name="backup_conflict_removed">Removed (%1$s)</string> - <string name="backup_conflict_added">Added (%1$s)</string> - <string name="backup_conflict_changed">Changed (%1$s)</string> - <string name="backup_conflict_private_cleared">You removed all private (encrypted) items.</string> - <string name="backup_conflict_private_changed">You also changed private (encrypted) items. They can\'t be compared here.</string> - <string name="backup_conflict_private_added">You added private (encrypted) items.</string> - <string name="backup_conflict_restore_mine">Restore saved version</string> - <string name="backup_review_saved">On this device</string> - <string name="backup_review_new">New version</string> - <string name="backup_review_not_set">Not set</string> - <string name="backup_conflict_count_removed">%1$s removed</string> - <string name="backup_conflict_count_added">%1$s added</string> - <string name="backup_conflict_count_changed">%1$s changed</string> - <plurals name="backup_card_follows_dropped"> - <item quantity="one">You unfollowed %1$d person</item> - <item quantity="other">You unfollowed %1$d people</item> - </plurals> - <string name="backup_card_accept">Accept</string> - <string name="backup_review_title_updated">%1$s updated</string> - <string name="backup_review_title_cleared">%1$s cleared</string> - <string name="backup_review_changed_by_other_app">Changed in another app · %1$s</string> - <string name="backup_action_keep_count">Keep %1$s</string> - <string name="backup_action_restore_count">Restore %1$s</string> - <string name="backup_action_remute">Re-mute %1$s</string> - <string name="backup_action_rejoin">Rejoin %1$s</string> - <string name="backup_action_restore_profile">Restore profile</string> - <string name="backup_action_restore_relays">Restore relays</string> - <string name="backup_review_kept_count">%1$s kept</string> - <string name="backup_review_dropped_count">%1$s dropped</string> - <string name="backup_review_new_count">%1$s new</string> - <string name="backup_review_tab_dropped">Dropped · %1$s</string> - <string name="backup_review_tab_new">New · %1$s</string> - <string name="backup_review_tab_edited">Edited · %1$s</string> - <string name="backup_review_search_people">Search %1$s people</string> - <string name="backup_review_nothing_here">Nothing here</string> - <string name="backup_review_unmuted">%1$s unmuted</string> - <string name="backup_review_unmuted_explainer">These can reach your feeds again</string> - <string name="backup_review_newly_muted">%1$s newly muted</string> - <string name="backup_review_words_and_hashtags">Words & hashtags</string> - <string name="backup_review_names_and_more">%1$s and %2$s more</string> - <string name="backup_review_private_wiped_title">You removed all private items</string> - <string name="backup_review_private_wiped_body">They\'re encrypted, so only you can see what they were. Restoring brings them all back.</string> - <string name="backup_review_private_added_title">You made items private</string> - <string name="backup_review_private_added_body">They\'re encrypted now, which is likely where the public items above went.</string> - <string name="backup_review_private_changed_title">You changed private items</string> - <string name="backup_review_private_changed_body">They\'re encrypted, so they can\'t be compared here.</string> - <string name="backup_review_gone">Gone</string> - <string name="backup_review_new_tag">New</string> - <string name="backup_review_dropped_tag">Dropped</string> - <string name="backup_review_demoted_tag">Moved</string> - <string name="backup_review_units_tag">Units</string> - <string name="backup_review_left_tag">Left</string> - <string name="backup_review_renamed_tag">Renamed</string> - <string name="backup_review_outbox">Outbox</string> - <string name="backup_review_outbox_explainer">where you publish</string> - <string name="backup_review_inbox">Inbox</string> - <string name="backup_review_inbox_explainer">where others reach you</string> - <string name="backup_review_key_replaced">Receiving key replaced</string> - <string name="backup_review_key_removed">Receiving key removed</string> - <string name="backup_review_key_warning">Nutzaps will be locked to this new key. If this device doesn\'t have it, you may not be able to redeem them here.</string> - <string name="backup_review_left">left</string> - <string name="backup_review_joined">joined</string> - <string name="backup_review_renamed">renamed</string> - <string name="backup_review_was_named">was “%1$s”</string> - <string name="backup_action_block_again">Block %1$s again</string> - <string name="backup_action_restore_feeds">Restore feeds</string> - <string name="backup_action_restore_topics">Restore topics</string> - <string name="backup_action_restore_places">Restore places</string> - <string name="backup_action_restore_providers">Restore providers</string> - <string name="backup_action_restore_wallet">Restore wallet</string> - <string name="backup_review_kept">kept</string> - <string name="backup_review_receiving_tag">Receiving</string> - <string name="backup_review_listed_tag">Listed</string> - <string name="backup_review_searching_tag">Searching</string> - <string name="backup_review_storing_tag">Storing</string> - <string name="backup_review_trusted_tag">Trusted</string> - <string name="backup_review_untrusted_tag">Untrusted</string> - <string name="backup_review_unblocked">%1$s unblocked</string> - <string name="backup_review_unblocked_explainer">Amethyst will connect to them again</string> - <string name="backup_review_no_longer_blocked">No longer blocked</string> - <string name="backup_review_newly_blocked">Newly blocked</string> - <string name="backup_review_still_blocked">Still blocked</string> - <string name="backup_review_allowed_tag">Allowed</string> - <string name="backup_review_moved_tag">Moved</string> - <string name="backup_review_feed">Feed</string> - <string name="backup_review_topics_gone">topics gone</string> - <string name="backup_review_places_gone">places gone</string> - <string name="backup_review_new_count_caption">new</string> - <string name="backup_review_hashtags_you_follow">Hashtags you follow</string> - <string name="backup_review_places_you_follow">Places you follow</string> - <string name="backup_review_services_changed">%1$s of %2$s services changed</string> - <string name="backup_review_services_changed_explainer">They now come from someone else, or no one</string> - <string name="backup_review_service">Service</string> - <string name="backup_review_service_rank">Rank</string> - <string name="backup_review_service_followers">Followers</string> - <string name="backup_review_service_topics">Topics</string> - <string name="backup_review_trust_warning">Ranks, follower counts and spam filtering you see come from these providers.</string> - <plurals name="backup_review_payment_targets_gone"> - <item quantity="one">%1$d way to pay you removed</item> - <item quantity="other">%1$d ways to pay you removed</item> - </plurals> - <plurals name="backup_review_offers_gone"> - <item quantity="one">%1$d offer removed</item> - <item quantity="other">%1$d offers removed</item> - </plurals> - <string name="backup_review_payments_explainer">People paying you only see what\'s in this list.</string> - <string name="backup_review_offer">BOLT12 offer</string> - <string name="backup_review_encrypted_cleared">You emptied it</string> - <string name="backup_review_encrypted_added">You created it</string> - <string name="backup_review_encrypted_changed">You changed it</string> - <string name="backup_review_wallet_body">Your wallet\'s mints and keys are encrypted to you, so they can\'t be compared item by item here. Restoring puts back exactly what this device saved.</string> - <string name="backup_review_concord_body">Your Concord communities are encrypted to you, so they can\'t be compared here. Restoring puts back exactly what this device saved.</string> - <plurals name="backup_card_mutes_visible"> - <item quantity="one">You unmuted %1$d thing</item> - <item quantity="other">You unmuted %1$d things</item> - </plurals> - <plurals name="backup_card_profile_lost_fields"> - <item quantity="one">You removed %1$d profile field</item> - <item quantity="other">You removed %1$d profile fields</item> - </plurals> - <plurals name="backup_card_nip65_lost"> - <item quantity="one">You removed %1$d public relay</item> - <item quantity="other">You removed %1$d public relays</item> - </plurals> - <plurals name="backup_card_dm_relays_lost"> - <item quantity="one">You removed %1$d DM relay</item> - <item quantity="other">You removed %1$d DM relays</item> - </plurals> - <plurals name="backup_card_key_package_relays_lost"> - <item quantity="one">You removed %1$d group-chat key relay</item> - <item quantity="other">You removed %1$d group-chat key relays</item> - </plurals> - <plurals name="backup_card_search_relays_lost"> - <item quantity="one">You removed %1$d search relay</item> - <item quantity="other">You removed %1$d search relays</item> - </plurals> - <plurals name="backup_card_indexer_relays_lost"> - <item quantity="one">You removed %1$d indexer relay</item> - <item quantity="other">You removed %1$d indexer relays</item> - </plurals> - <plurals name="backup_card_relay_feeds_lost"> - <item quantity="one">You removed %1$d relay feed</item> - <item quantity="other">You removed %1$d relay feeds</item> - </plurals> - <plurals name="backup_card_private_outbox_lost"> - <item quantity="one">You removed %1$d private relay</item> - <item quantity="other">You removed %1$d private relays</item> - </plurals> - <plurals name="backup_card_trusted_lost"> - <item quantity="one">You stopped trusting %1$d relay</item> - <item quantity="other">You stopped trusting %1$d relays</item> - </plurals> - <plurals name="backup_card_unblocked"> - <item quantity="one">You unblocked %1$d relay</item> - <item quantity="other">You unblocked %1$d relays</item> - </plurals> - <plurals name="backup_card_leave_chats"> - <item quantity="one">You left %1$d public chat</item> - <item quantity="other">You left %1$d public chats</item> - </plurals> - <plurals name="backup_card_leave_communities"> - <item quantity="one">You left %1$d community</item> - <item quantity="other">You left %1$d communities</item> - </plurals> - <plurals name="backup_card_leave_rooms"> - <item quantity="one">You left %1$d chat room</item> - <item quantity="other">You left %1$d chat rooms</item> - </plurals> - <plurals name="backup_card_leave_groups"> - <item quantity="one">You left %1$d group</item> - <item quantity="other">You left %1$d groups</item> - </plurals> - <plurals name="backup_card_feeds_removed"> - <item quantity="one">You removed %1$d favorite feed</item> - <item quantity="other">You removed %1$d favorite feeds</item> - </plurals> - <plurals name="backup_card_hashtags_unfollowed"> - <item quantity="one">You unfollowed %1$d hashtag</item> - <item quantity="other">You unfollowed %1$d hashtags</item> - </plurals> - <plurals name="backup_card_places_unfollowed"> - <item quantity="one">You unfollowed %1$d place</item> - <item quantity="other">You unfollowed %1$d places</item> - </plurals> - <plurals name="backup_card_trust_services_changed"> - <item quantity="one">You changed the provider of %1$d trust service</item> - <item quantity="other">You changed the provider of %1$d trust services</item> - </plurals> - <plurals name="backup_card_nutzap_mints_removed"> - <item quantity="one">You removed %1$d nutzap mint</item> - <item quantity="other">You removed %1$d nutzap mints</item> - </plurals> - <plurals name="backup_card_payment_targets_removed"> - <item quantity="one">You removed %1$d way to get paid</item> - <item quantity="other">You removed %1$d ways to get paid</item> - </plurals> - <plurals name="backup_card_offers_removed"> - <item quantity="one">You removed %1$d BOLT12 offer</item> - <item quantity="other">You removed %1$d BOLT12 offers</item> - </plurals> - <plurals name="backup_card_and_added"> - <item quantity="one">%1$s and added %2$d</item> - <item quantity="other">%1$s and added %2$d</item> - </plurals> - <plurals name="backup_card_and_followed"> - <item quantity="one">%1$s and followed %2$d</item> - <item quantity="other">%1$s and followed %2$d</item> - </plurals> - <plurals name="backup_card_and_muted"> - <item quantity="one">%1$s and muted %2$d</item> - <item quantity="other">%1$s and muted %2$d</item> - </plurals> - <plurals name="backup_card_and_joined"> - <item quantity="one">%1$s and joined %2$d</item> - <item quantity="other">%1$s and joined %2$d</item> - </plurals> - <plurals name="backup_card_and_blocked"> - <item quantity="one">%1$s and blocked %2$d</item> - <item quantity="other">%1$s and blocked %2$d</item> - </plurals> - <string name="backup_card_app_settings">Your Amethyst synced app settings changed in another app that is not Amethyst. This can create problems.</string> - <string name="backup_card_nutzap_key_replaced">You replaced your nutzap key</string> - <string name="backup_card_nutzap_key_removed">You removed your nutzap key</string> - <string name="backup_card_wallet_emptied">You emptied your Cashu wallet</string> - <string name="backup_card_wallet_rewritten">You changed your Cashu wallet</string> - <string name="backup_card_concord_emptied">You emptied your Concord list</string> - <string name="backup_card_concord_rewritten">You changed your Concord list</string> - <string name="backup_conflict_keep_new">Use new version</string> - <string name="backup_conflict_relay_read_only">read only</string> - <string name="backup_conflict_relay_write_only">write only</string> - <string name="backup_conflict_relay_read_write">read and write</string> - <string name="backup_type_profile">profile</string> - <string name="backup_type_profile_explainer">Your name, picture, bio and payment addresses, as everyone sees them.</string> - <string name="backup_type_follow_list">follow list</string> - <string name="backup_type_follow_list_explainer">Decides whose posts appear in your home feed.</string> - <string name="backup_type_mute_list">mute list</string> - <string name="backup_type_mute_list_explainer">Hides people, words, hashtags and threads from everything you see.</string> - <string name="backup_type_outbox_inbox_relays">public relay list</string> - <string name="backup_type_outbox_inbox_relays_explainer">Tells everyone where to find your posts and where to send you replies and mentions.</string> - <string name="backup_type_dm_relays">DM relay list</string> - <string name="backup_type_dm_relays_explainer">Where others deliver your private messages.</string> - <string name="backup_type_key_package_relays">group-chat key relays</string> - <string name="backup_type_key_package_relays_explainer">Where others find the keys to add you to encrypted group chats.</string> - <string name="backup_type_search_relays">search relay list</string> - <string name="backup_type_search_relays_explainer">The relays used when you search.</string> - <string name="backup_type_indexer_relays">indexer relay list</string> - <string name="backup_type_indexer_relays_explainer">The relays used to find other people\'s profiles and relay lists.</string> - <string name="backup_type_relay_feeds">favorite relay feeds</string> - <string name="backup_type_relay_feeds_explainer">The relays you browse as feeds.</string> - <string name="backup_type_blocked_relays">blocked relay list</string> - <string name="backup_type_blocked_relays_explainer">Relays this app never connects to.</string> - <string name="backup_type_trusted_relays">trusted relay list</string> - <string name="backup_type_trusted_relays_explainer">Relays you trust to connect to without asking.</string> - <string name="backup_type_private_outbox_relays">private relay list</string> - <string name="backup_type_private_outbox_relays_explainer">Where your drafts and private data are stored.</string> - <string name="backup_type_app_settings">synced app settings</string> - <string name="backup_type_app_settings_explainer">Amethyst settings synced across your devices. They are encrypted, so only their presence can be compared.</string> - <string name="backup_type_public_chats">public chat list</string> - <string name="backup_type_public_chats_explainer">The public chats you joined.</string> - <string name="backup_type_communities">community list</string> - <string name="backup_type_communities_explainer">The communities you joined.</string> - <string name="backup_type_hashtags">followed hashtags</string> - <string name="backup_type_hashtags_explainer">Hashtags whose posts appear in your feeds.</string> - <string name="backup_type_geohashes">followed locations</string> - <string name="backup_type_geohashes_explainer">Locations whose posts appear in your feeds.</string> - <string name="backup_type_favorite_algo_feeds">favorite feeds list</string> - <string name="backup_type_favorite_algo_feeds_explainer">The algorithmic feeds you saved.</string> - <string name="backup_type_ephemeral_chats">ephemeral chat list</string> - <string name="backup_type_ephemeral_chats_explainer">The ephemeral chat rooms you joined.</string> - <string name="backup_type_relay_groups">group list</string> - <string name="backup_type_relay_groups_explainer">The relay-based groups you joined.</string> - <string name="backup_type_concord_communities">Concord community list</string> - <string name="backup_type_concord_communities_explainer">The Concord communities you joined. They are encrypted, so only their presence can be compared.</string> - <string name="backup_type_trust_providers">trust provider list</string> - <string name="backup_type_trust_providers_explainer">The services you trust to rank and verify people for you.</string> - <string name="backup_type_cashu_wallet">Cashu wallet</string> - <string name="backup_type_cashu_wallet_explainer">Your ecash wallet settings and mints. They are encrypted, so only their presence can be compared.</string> - <string name="backup_type_nutzap_info">nutzap receiving settings</string> - <string name="backup_type_nutzap_info_explainer">The mints and key others use to send you nutzaps.</string> - <string name="backup_type_payment_targets">payment addresses</string> - <string name="backup_type_payment_targets_explainer">The ways other people can pay you.</string> - <string name="backup_type_bolt12_offers">BOLT12 offers</string> - <string name="backup_type_bolt12_offers_explainer">The lightning offers others use to pay you.</string> - <string name="backup_type_other">list</string> - <string name="backup_type_other_explainer">A list saved on this device.</string> - <string name="backup_entry_profile_field">Profile fields</string> - <string name="backup_entry_person">People</string> - <string name="backup_entry_hashtag">Hashtags</string> - <string name="backup_entry_word">Words</string> - <string name="backup_entry_thread">Threads</string> - <string name="backup_entry_public_chat">Public chats</string> - <string name="backup_entry_community">Communities</string> - <string name="backup_entry_algo_feed">Feeds</string> - <string name="backup_entry_location">Locations</string> - <string name="backup_entry_relay">Relays</string> - <string name="backup_entry_relay_group">Groups</string> - <string name="backup_entry_chat_room">Chat rooms</string> - <string name="backup_entry_trust_provider">Trust providers</string> - <string name="backup_entry_mint">Mints</string> - <string name="backup_entry_nutzap_key">Nutzap keys</string> - <string name="backup_entry_payment_target">Payment addresses</string> - <string name="backup_entry_offer">Offers</string> - <string name="backup_entry_identity_claim">Linked identities</string> - <string name="backup_entry_other">Other entries</string> - <string name="backup_profile_field_name">Username</string> - <string name="backup_profile_field_display_name">Display name</string> - <string name="backup_profile_field_about">About</string> - <string name="backup_profile_field_picture">Picture</string> - <string name="backup_profile_field_banner">Banner</string> - <string name="backup_profile_field_website">Website</string> - <string name="backup_profile_field_nip05">Nostr address</string> - <string name="backup_profile_field_lud16">Lightning address</string> - <string name="backup_profile_field_lud06">LNURL</string> - <string name="backup_profile_field_pronouns">Pronouns</string> - <string name="backup_profile_field_birthday">Birthday</string> - <string name="backup_profile_field_clink_offer">CLINK offer</string> - <string name="backup_profile_field_bot">Bot flag</string> - <plurals name="cordn_info_admins_kept"> - <item quantity="one">The group\'s %1$d admin stays as it is.</item> - <item quantity="other">The group\'s %1$d admins stay as they are.</item> - </plurals> </resources> diff --git a/commonsUI/ARCHITECTURE.md b/commonsUI/ARCHITECTURE.md index b693e06a2b..8a091c05f7 100644 --- a/commonsUI/ARCHITECTURE.md +++ b/commonsUI/ARCHITECTURE.md @@ -58,7 +58,10 @@ is specific to the UI module. `tools/icon-font/build_icon_font.py` read `icons/` and write `composeResources/font/` in this module (see root `.claude/CLAUDE.md`, "Icons"). - Translations: `crowdin.yml` and `tools/strings-migrate/` target - `commonsUI/src/commonMain/composeResources/`. + `commonsUI/src/commonMain/composeResources/`. This is where **every new + user-visible string** goes, Android-only screens included; the app's own + `res/values/strings.xml` holds only the synchronous-platform tier (see root + `.claude/CLAUDE.md`, "Strings"). - CI: `.github/workflows/build.yml` runs `:commonsUI:jvmTest`, `:commonsUI:verifyKmpPurity` and the iOS compile/test tasks next to the `:commons` ones. diff --git a/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml b/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml index 3a1f286d3d..2d16c9c513 100644 --- a/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml @@ -5286,4 +5286,322 @@ <string name="cyberspace_bag_unsigned">हस्ताक्षररहित। इसलिए यह कर्ता केवल एक कथन है</string> <string name="cyberspace_bag_unknown_kind">प्रकार %1$d का वस्तु। जिसे यह ग्राहक चित्रित नहीं करता</string> <string name="cyberspace_bag_opaque">%1$d अष्टकों का अमुक जिसे यह ग्राहक पढता नहीं</string> + <string name="backup_action_block_again">पुनःबाधित करें %1$s को</string> + <string name="backup_action_keep_count">रखें %1$s</string> + <string name="backup_action_rejoin">पुनःजुडें %1$s</string> + <string name="backup_action_remute">पुनःमौन %1$s</string> + <string name="backup_action_restore_count">पुनःस्थापन %1$s</string> + <string name="backup_action_restore_feeds">सूचनावलियाँ पुनःस्थापन</string> + <string name="backup_action_restore_places">स्थल पुनःस्थापन</string> + <string name="backup_action_restore_profile">परिचय पुनःस्थापध</string> + <string name="backup_action_restore_providers">प्रदाता पुनःस्थापन</string> + <string name="backup_action_restore_relays">पुनःप्रसारक पुनःस्थापन</string> + <string name="backup_action_restore_topics">विषय पुनःस्थापन</string> + <string name="backup_action_restore_wallet">धनकोष पुनःस्थापन</string> + <string name="backup_card_accept">स्वीकार</string> + <plurals name="backup_card_and_added"> + <item quantity="one">%1$s तथा %2$d जोडा</item> + <item quantity="other">%1$s तथा %2$d जोडे</item> + </plurals> + <plurals name="backup_card_and_blocked"> + <item quantity="one">%1$s तथा %2$d बाधित किया</item> + <item quantity="other">%1$s तथा %2$d को बाधित किए</item> + </plurals> + <plurals name="backup_card_and_followed"> + <item quantity="one">%1$s तथा %2$d का अनुगमन किया</item> + <item quantity="other">%1$s तथा %2$d का अनुगमन किए</item> + </plurals> + <plurals name="backup_card_and_joined"> + <item quantity="one">%1$s तथा %2$d से जुडे</item> + <item quantity="other">%1$s तथा %2$d से जुडे</item> + </plurals> + <plurals name="backup_card_and_muted"> + <item quantity="one">%1$s तथा %2$d को मौनकृत किया</item> + <item quantity="other">%1$s तथा %2$d को मौनकृत किए</item> + </plurals> + <string name="backup_card_app_settings">आपके अमेथिस्ट समचरणीकृत क्रमक स्थापना विकल्पों को परिवर्तित किया गया एक अन्य क्रमक में और वह अमेथिस्ट नहीं था। इससे समस्याएँ उत्पन्न हो सकते हैं।</string> + <string name="backup_card_concord_emptied">आपने अपनी कोंकोर्ड सूची रिक्त की</string> + <string name="backup_card_concord_rewritten">आपने अपनी कोंकोर्ड सूची का परिवर्तन किया</string> + <plurals name="backup_card_dm_relays_lost"> + <item quantity="one">आपने %1$d सीधासन्देश पुनःप्रसारक हटाया</item> + <item quantity="other">आपने %1$d सीधासन्देश पुनःप्रसारकों को हटाए</item> + </plurals> + <plurals name="backup_card_feeds_removed"> + <item quantity="one">आपने %1$d इष्ट सूचनावली हटाया</item> + <item quantity="other">आपने %1$d इष्ट सूचनावलियों को हटाए</item> + </plurals> + <plurals name="backup_card_follows_dropped"> + <item quantity="one">आपने %1$d व्यक्ति का अनुगमन हटाया</item> + <item quantity="other">आपने %1$d व्यक्तियों का अनुगमन हटाए</item> + </plurals> + <plurals name="backup_card_hashtags_unfollowed"> + <item quantity="one">आपने %1$d विषयसूचक का अनुगमन हटाया</item> + <item quantity="other">आपने %1$d विषयसूचकों का अनुगमन हटाए</item> + </plurals> + <plurals name="backup_card_indexer_relays_lost"> + <item quantity="one">आपने %1$d अनुक्रमणीकार पुनःप्रसारक हटाया</item> + <item quantity="other">आपने %1$d अनुक्रमणीकार पुनःप्रसारकों को हटाए</item> + </plurals> + <plurals name="backup_card_key_package_relays_lost"> + <item quantity="one">आपने %1$d समूहचर्चा कुंचिका पुनःप्रसारक हटाया</item> + <item quantity="other">आपने %1$d समूहचर्चा कुंचिका पुनःप्रसारकों को हटाए</item> + </plurals> + <plurals name="backup_card_leave_chats"> + <item quantity="one">आपने %1$d सार्वजनिक चर्चा छोड दिया</item> + <item quantity="other">आपने %1$d सार्वजनिक चर्चाओं को छोड दिए</item> + </plurals> + <plurals name="backup_card_leave_communities"> + <item quantity="one">आपने %1$d समुदाय छोड दिया</item> + <item quantity="other">आपने %1$d समुदायों को छोड दिए</item> + </plurals> + <plurals name="backup_card_leave_groups"> + <item quantity="one">आपने %1$d समूह छोड दिया</item> + <item quantity="other">आपने %1$d समूहों को छोड दिए</item> + </plurals> + <plurals name="backup_card_leave_rooms"> + <item quantity="one">आपने %1$d चर्चाशाला छोड दिया</item> + <item quantity="other">आपने %1$d चर्चाशालाओं को छोड दिए</item> + </plurals> + <plurals name="backup_card_mutes_visible"> + <item quantity="one">आपने %1$d की मौनावस्था हटाया</item> + <item quantity="other">आपने %1$d की मौनावस्था हटाए</item> + </plurals> + <plurals name="backup_card_nip65_lost"> + <item quantity="one">आपने %1$d सार्वजनिक पुनःप्रसारक हटाया</item> + <item quantity="other">आपने %1$d सार्वजनिक पुनःप्रसारकों को हटाए</item> + </plurals> + <string name="backup_card_nutzap_key_removed">आपने अपनी नटज्साप कुंचिका को हटाया</string> + <string name="backup_card_nutzap_key_replaced">आपने अपनी नटज्साप कुंचिका का प्रतिस्थापन किया</string> + <plurals name="backup_card_nutzap_mints_removed"> + <item quantity="one">आपने %1$d नटज्साप टकसाल हटाया</item> + <item quantity="other">आपने %1$d नटज्साप टकसालों को हटाए</item> + </plurals> + <plurals name="backup_card_offers_removed"> + <item quantity="one">आपने %1$d बोल्ट॰बारह प्रस्ताव हटाया</item> + <item quantity="other">आपने %1$d बोल्ट॰बारह प्रस्तावों को हटाए</item> + </plurals> + <plurals name="backup_card_payment_targets_removed"> + <item quantity="one">आपने %1$d भुगतान प्राप्त करने की विधि हटाया</item> + <item quantity="other">आपने %1$d भुगतान प्राप्त करने के विधियों को हटाए</item> + </plurals> + <plurals name="backup_card_places_unfollowed"> + <item quantity="one">आपने %1$d स्थल का अनुगमन हटाया</item> + <item quantity="other">आपने %1$d स्थलों का अनुगमन हटाए</item> + </plurals> + <plurals name="backup_card_private_outbox_lost"> + <item quantity="one">आपने %1$d निजी पुनःप्रसारक हटाया</item> + <item quantity="other">आपने %1$d निजी पुनःप्रसारकों को हटाए</item> + </plurals> + <plurals name="backup_card_profile_lost_fields"> + <item quantity="one">आपने %1$d परिचय प्रपत्रस्थान हटाया</item> + <item quantity="other">आपने %1$d परिचय प्रपत्रस्थान हटाए</item> + </plurals> + <plurals name="backup_card_relay_feeds_lost"> + <item quantity="one">आपने %1$d पुनःप्रसारक सूचनावली हटाया</item> + <item quantity="other">आपने %1$d पुनःप्रसारक सूचनावलियाँ हटाए</item> + </plurals> + <plurals name="backup_card_search_relays_lost"> + <item quantity="one">आपने %1$d खोज पुनःप्रसारक हटाया</item> + <item quantity="other">आपने %1$d खोज पुनःप्रसारकों को हटाए</item> + </plurals> + <plurals name="backup_card_trust_services_changed"> + <item quantity="one">आपने %1$d विश्वाससेवा प्रदाता परिवर्तित किया</item> + <item quantity="other">आपने %1$d विश्वाससेवाओं के प्रदाता को परिवर्तित किया</item> + </plurals> + <plurals name="backup_card_trusted_lost"> + <item quantity="one">आपने %1$d पुनःप्रसारक पर विश्वास करना रोक दिया</item> + <item quantity="other">आपने %1$d पुनःप्रसारकों पर विश्वास करना रोक दिए</item> + </plurals> + <plurals name="backup_card_unblocked"> + <item quantity="one">आपने %1$d पुनःप्रसारक अबाधित किया</item> + <item quantity="other">आपने %1$d पुनःप्रसारकों को अबाधित किए</item> + </plurals> + <string name="backup_card_wallet_emptied">आपने अपना काशयू धनकोष रिक्त किया</string> + <string name="backup_card_wallet_rewritten">आपने अपने काशयू धनकोष को परिवर्तित किया</string> + <string name="backup_conflict_added">जोडे गए (%1$s)</string> + <string name="backup_conflict_changed">परिवर्तित (%1$s)</string> + <string name="backup_conflict_count_added">%1$s जोडे गए</string> + <string name="backup_conflict_count_changed">%1$s परिवर्तित</string> + <string name="backup_conflict_count_removed">%1$s हटाए गए</string> + <string name="backup_conflict_keep_new">नया संस्करण का उपयोग करें</string> + <string name="backup_conflict_private_added">आपने निजी (रहस्यीकृत) वस्तुओं को जोडा।</string> + <string name="backup_conflict_private_changed">आपने निजी (रहस्यीकृत) वस्तुओं का परिवर्तन भी किया। उनकी तुलना यहाँ नहीं की जा सकती।</string> + <string name="backup_conflict_private_cleared">आपने सभी निजी (रहस्यीकृत) वस्तुओं को हटा दिया।</string> + <string name="backup_conflict_relay_read_only">पठनेवशक्य</string> + <string name="backup_conflict_relay_read_write">पठन तथा लेखन</string> + <string name="backup_conflict_relay_write_only">लेखनेवशक्य</string> + <string name="backup_conflict_removed">हटाए गए (%1$s)</string> + <string name="backup_conflict_restore_mine">सुरक्षित संस्करण पुनःस्थापन</string> + <string name="backup_conflict_title">आपने अपना %1$s अन्य क्रमक में परिवर्तित किया</string> + <string name="backup_entry_algo_feed">सूचनावलियाँ</string> + <string name="backup_entry_chat_room">चर्चा शालाएँ</string> + <string name="backup_entry_community">समुदाय</string> + <string name="backup_entry_hashtag">विषयसूचक</string> + <string name="backup_entry_identity_claim">सम्बन्धित व्यक्तिगण</string> + <string name="backup_entry_location">स्थान</string> + <string name="backup_entry_mint">टकसाल</string> + <string name="backup_entry_nutzap_key">नटज्साप कुंचिकाएँ</string> + <string name="backup_entry_offer">प्रस्ताव</string> + <string name="backup_entry_other">अन्य प्रविष्टियाँ</string> + <string name="backup_entry_payment_target">भुगतान पते</string> + <string name="backup_entry_person">लोग</string> + <string name="backup_entry_profile_field">परिचय प्रपत्रस्थान</string> + <string name="backup_entry_public_chat">सार्वजनिक चर्चाएँ</string> + <string name="backup_entry_relay">पुनःप्रसारक</string> + <string name="backup_entry_relay_group">समूह</string> + <string name="backup_entry_thread">सूत्र</string> + <string name="backup_entry_trust_provider">विश्वाससेवा प्रदाता</string> + <string name="backup_entry_word">शब्द</string> + <string name="backup_profile_field_about">जानकारी</string> + <string name="backup_profile_field_banner">ध्वज</string> + <string name="backup_profile_field_birthday">जन्मदिन</string> + <string name="backup_profile_field_bot">क्रमचर चिह्न</string> + <string name="backup_profile_field_clink_offer">क्लिंक॰ प्रस्ताव</string> + <string name="backup_profile_field_display_name">प्रदर्शन नाम</string> + <string name="backup_profile_field_lud06">लैटनिंग जालपता</string> + <string name="backup_profile_field_lud16">लैटनिंग पता</string> + <string name="backup_profile_field_name">प्रयोक्तानाम</string> + <string name="backup_profile_field_nip05">नोस्टर पता</string> + <string name="backup_profile_field_picture">चित्र</string> + <string name="backup_profile_field_pronouns">सर्वनाम</string> + <string name="backup_profile_field_website">जालस्थान</string> + <string name="backup_review_allowed_tag">अनुमत</string> + <string name="backup_review_changed_by_other_app">अन्य क्रमक में परिवर्तित। %1$s</string> + <string name="backup_review_concord_body">आपके कोंकोर्ड समुदाय आपके लिए रहस्यीकृत हैं। इसलिए उनकी तुलना यहाँ नहीं की जा सकती। पुनःस्थापन उन्हीं को लौट डालेगा जिनका अभिलेखन इस यन्त्र ने किया।</string> + <string name="backup_review_demoted_tag">स्थानान्तरित</string> + <string name="backup_review_dropped_count">%1$s परित्यक्त</string> + <string name="backup_review_dropped_tag">परित्यक्त</string> + <string name="backup_review_encrypted_added">आपने उसे बनाया</string> + <string name="backup_review_encrypted_changed">आपने उसे परिवर्तित किया</string> + <string name="backup_review_encrypted_cleared">आपने उसे रिक्त किया</string> + <string name="backup_review_feed">सूचनावली</string> + <string name="backup_review_gone">चला गया</string> + <string name="backup_review_hashtags_you_follow">विषयसूचक जिनका आप अनुगमन करते हैं</string> + <string name="backup_review_inbox">आगतपेटिका</string> + <string name="backup_review_inbox_explainer">जहाँ अन्य आप तक अभिगमन करते हैं</string> + <string name="backup_review_joined">जुड गए</string> + <string name="backup_review_kept">रखे गए</string> + <string name="backup_review_kept_count">%1$s रखे गए</string> + <string name="backup_review_key_removed">ग्राहक कुंचिका हटाया गया</string> + <string name="backup_review_key_replaced">ग्राहक कुंचिका प्रतिस्थापित</string> + <string name="backup_review_key_warning">नटज्साप आगे से इस कुंचिका से बद्ध होंगे। यदि इस यन्त्र में वह नहीं है तो आप सम्भाव्यतः उनको यहाँ नहीं चुका सकते।</string> + <string name="backup_review_left">शेष</string> + <string name="backup_review_left_tag">शेष</string> + <string name="backup_review_listed_tag">सूचीबद्ध</string> + <string name="backup_review_moved_tag">स्थानान्तरित</string> + <string name="backup_review_names_and_more">%1$s तथा %2$s अधिक</string> + <string name="backup_review_new">नया संस्करण</string> + <string name="backup_review_new_count">%1$s नए</string> + <string name="backup_review_new_count_caption">नव्य</string> + <string name="backup_review_new_tag">नव्य</string> + <string name="backup_review_newly_blocked">नव्यतः बाधित</string> + <string name="backup_review_newly_muted">%1$s नए मौनकृत</string> + <string name="backup_review_no_longer_blocked">अब से बाधित नहीं</string> + <string name="backup_review_not_set">स्थापित नहीं</string> + <string name="backup_review_nothing_here">यहाँ कुछ नहीं</string> + <string name="backup_review_offer">बोल्ट॰बारह प्रस्ताव</string> + <plurals name="backup_review_offers_gone"> + <item quantity="one">%1$d प्रस्ताव हटाया गया</item> + <item quantity="other">%1$d प्रस्ताव हटाए गए</item> + </plurals> + <string name="backup_review_outbox">निर्गतपेटिका</string> + <string name="backup_review_outbox_explainer">आप जहाँ प्रकाशित करते हैं</string> + <plurals name="backup_review_payment_targets_gone"> + <item quantity="one">आपको भुगतान करने की %1$d विधि हटाया गया</item> + <item quantity="other">आपको भुगतान करने की %1$d विधियाँ हटाए गए</item> + </plurals> + <string name="backup_review_payments_explainer">लोग जो आपको भुगतान करते हैं उसी को देखते हैं जो इस सूची में है।</string> + <string name="backup_review_places_gone">स्थल हटाए गए</string> + <string name="backup_review_places_you_follow">स्थल जिनका आप अनुगमन करते हैं</string> + <string name="backup_review_private_added_body">वे अब रहस्यीकृत हैं। जो कि सम्भाव्यतः ऊपर के सार्वजनिक वस्तुओं का गन्तव्य था।</string> + <string name="backup_review_private_added_title">आपने वस्तुओं को निजी किया</string> + <string name="backup_review_private_changed_body">वे रहस्यीकृत हैं। इसलिए उनकी तुलना यहाँ नहीं की जा सकती।</string> + <string name="backup_review_private_changed_title">आपने निजी वस्तुओं का परिवर्तन किया</string> + <string name="backup_review_private_wiped_body">वे रहस्यीकृत हैं। इसलिए केवल आप देख सकते हैं कि वे क्या थे। पुनःस्थापन से सब लौटके आ जाएँगे।</string> + <string name="backup_review_private_wiped_title">आपने सभी निजी वस्तुओं को हटा दिया</string> + <string name="backup_review_receiving_tag">ग्रहण मध्य</string> + <string name="backup_review_renamed">पुनःनामकृत</string> + <string name="backup_review_renamed_tag">पुनःनामकृत</string> + <string name="backup_review_saved">इस यन्त्र पर</string> + <string name="backup_review_search_people">%1$s लोग खोजें</string> + <string name="backup_review_searching_tag">खोज मध्य</string> + <string name="backup_review_service">सेवा</string> + <string name="backup_review_service_followers">अनुचर</string> + <string name="backup_review_service_rank">श्रेणी</string> + <string name="backup_review_service_topics">विषयसूची</string> + <string name="backup_review_services_changed">%2$s में से %1$s सेवाएँ परिवर्तित</string> + <string name="backup_review_services_changed_explainer">वे अब अन्य व्यक्ति से आते है। अथवा किसी से नहीं</string> + <string name="backup_review_still_blocked">अभी भी बाधित</string> + <string name="backup_review_storing_tag">अभिलेखन मध्य</string> + <string name="backup_review_tab_dropped">परित्यक्त। %1$s</string> + <string name="backup_review_tab_edited">सम्पादित। %1$s</string> + <string name="backup_review_tab_new">नए। %1$s</string> + <string name="backup_review_title_cleared">%1$s पारित</string> + <string name="backup_review_title_updated">%1$s अद्यतनकृत</string> + <string name="backup_review_topics_gone">विषय हटाए गए</string> + <string name="backup_review_trust_warning">श्रेणीयाँ तथा अनुचर गिनती तथा कचरालेख छालन जो आप देखते हैं इन प्रदाताओं से आते हैं।</string> + <string name="backup_review_trusted_tag">विश्वसनीय</string> + <string name="backup_review_unblocked">%1$s अबाधित</string> + <string name="backup_review_unblocked_explainer">अमेथिस्ट पुनः उनसे जुडेगा</string> + <string name="backup_review_units_tag">इकाई</string> + <string name="backup_review_unmuted">%1$s मौनावस्था हटाए गए</string> + <string name="backup_review_unmuted_explainer">ये पुनः आपके सूचनावलियों तक अभिगम्य</string> + <string name="backup_review_untrusted_tag">अविश्वसनीय</string> + <string name="backup_review_wallet_body">आपके धनकोष के टकसाल तथा कुंचिकाएँ आपके लिए रहस्यीकृत हैं। इसलिए उनकी तुलना वस्तु प्रति वस्तु यहाँ नहीं की जा सकती। पुनःस्थापन उन्हीं को लौट डालेगा जिनका अभिलेखन इस यन्त्र ने किया।</string> + <string name="backup_review_was_named">पूर्वतः %1$s था</string> + <string name="backup_review_words_and_hashtags">शब्द तथा विषयसूचक</string> + <string name="backup_type_app_settings">समचरणीकृत क्रमक स्थापना विकल्प</string> + <string name="backup_type_app_settings_explainer">अमेथिस्ट स्थापना विकल्प समचरणीकृत आपके यन्त्रों में। वे रहस्यीकृत हैं। इसलिए केवल उनकी उपस्थिति की तुलना की जा सकती है।</string> + <string name="backup_type_blocked_relays">बाधित पुनःप्रसारक सूची</string> + <string name="backup_type_blocked_relays_explainer">पुनःप्रसारक जिनसे यह क्रमक कभी भी संयोजन नहीं करता।</string> + <string name="backup_type_bolt12_offers">बोल्ट॰बारह प्रस्ताव</string> + <string name="backup_type_bolt12_offers_explainer">लैटनिंग प्रस्ताव जिनका उपयोग करके अन्य लोग आपको भुगतान करते हैं।</string> + <string name="backup_type_cashu_wallet">काशयू धनकोष</string> + <string name="backup_type_cashu_wallet_explainer">आपके ईकाश धनकोष स्थापना विकल्प तथा टकसाल। वे रहस्यीकृत हैं। इसलिए केवल उनकी उपस्थिति की तुलना की जा सकती है।</string> + <string name="backup_type_communities">समुदाय सूची</string> + <string name="backup_type_communities_explainer">समुदाय जिनसे आप जुडे।</string> + <string name="backup_type_concord_communities">कोंकोर्ड समुदाय सूची</string> + <string name="backup_type_concord_communities_explainer">कोंकोर्ड समुदाय जिनसे आप जुडे। वे रहस्यीकृत हैं। इसलिए केवल उनकी उपस्थिति की तुलना की जा सकती है।</string> + <string name="backup_type_dm_relays">सीधासन्देश पुनःप्रसारक सूची</string> + <string name="backup_type_dm_relays_explainer">जहाँ अन्य आपके निजी सन्देश भेजते हैं।</string> + <string name="backup_type_ephemeral_chats">अस्थायी चर्चा सूची</string> + <string name="backup_type_ephemeral_chats_explainer">अस्थायी चर्चा शालाएँ जिनसे आप जुडे।</string> + <string name="backup_type_favorite_algo_feeds">इष्ट सूचनावली सूची</string> + <string name="backup_type_favorite_algo_feeds_explainer">कलनविध्यात्मक सूचनावलियाँ जिनको आपने अभिलेखित किया।</string> + <string name="backup_type_follow_list">अनुचरण सूची</string> + <string name="backup_type_follow_list_explainer">निर्णय करता है किसके पत्र आपकी मुख्य सूचनावली में प्रकट होंगे।</string> + <string name="backup_type_geohashes">अनुचरित स्थान</string> + <string name="backup_type_geohashes_explainer">स्थान जहाँ से पत्र आपकी मुख्य सूचनावली में प्रकट होंगे।</string> + <string name="backup_type_hashtags">अनुचरित विषयसूचक</string> + <string name="backup_type_hashtags_explainer">विषयसूचक जिससे युक्त पत्र आपकी मुख्य सूचनावली में प्रकट होंगे।</string> + <string name="backup_type_indexer_relays">अनुक्रमणीकार पुनःप्रसारक सूची</string> + <string name="backup_type_indexer_relays_explainer">पुनःप्रसारक उपयुक्त अन्य लोगों के परिचय तथा पुनःप्रसारक सूचियों को प्राप्त करने के लिए।</string> + <string name="backup_type_key_package_relays">समूहचर्चा कुंचिका पुनःप्रसारक</string> + <string name="backup_type_key_package_relays_explainer">जहाँ अन्य उन कुंचिकाओं को प्राप्त करते हैं आपको रहस्यीकृत समूहचर्चाओं में जोडने के लिए।</string> + <string name="backup_type_mute_list">मौन सूची</string> + <string name="backup_type_mute_list_explainer">छिपाता है लोग शब्द विषयसूचक तथा सूत्र उन सब में से जो आप देखते हैं।</string> + <string name="backup_type_nutzap_info">नटज्साप प्राप्ती स्थापना विकल्प</string> + <string name="backup_type_nutzap_info_explainer">टकसाल तथा कुंचिका जिनका उपयोग अन्य करते हैं आपको नटज्साप भेजने के लिए।</string> + <string name="backup_type_other">सूची</string> + <string name="backup_type_other_explainer">इस यन्त्र पर अभिलेखित सूची।</string> + <string name="backup_type_outbox_inbox_relays">सार्वजनिक पुनःप्रसारक सूची</string> + <string name="backup_type_outbox_inbox_relays_explainer">सबको बताता है आपके पत्र कहाँ प्राप्त करें तथा आपको उत्तर तथा उल्लेखों को कहाँ भेजें।</string> + <string name="backup_type_payment_targets">भुगतान पते</string> + <string name="backup_type_payment_targets_explainer">विधियाँ जिनसे अन्य लोग आपको भुगतान कर सके।</string> + <string name="backup_type_private_outbox_relays">निजी पुनःप्रसारक सूची</string> + <string name="backup_type_private_outbox_relays_explainer">जहाँ आपके पाण्डुलिपियाँ तथा निजी जानकारी रखे जाते हैं।</string> + <string name="backup_type_profile">परिचय</string> + <string name="backup_type_profile_explainer">आपका नाम चित्र चरित तथा भुगतान पते। जैसे सब को दिखते हैं वैसे।</string> + <string name="backup_type_public_chats">सार्वजनिक चर्चा सूची</string> + <string name="backup_type_public_chats_explainer">सार्वजनिक चर्चाएँ जिनसे आप जुडे।</string> + <string name="backup_type_relay_feeds">इष्ट पुनःप्रसारक सूचनावलियाँ</string> + <string name="backup_type_relay_feeds_explainer">पुनःप्रसारक जिनका आप वीक्षण करते हैं सूचनावलियों के रूप में।</string> + <string name="backup_type_relay_groups">समूह सूची</string> + <string name="backup_type_relay_groups_explainer">पुनःप्रसारक आधारित समूह जिनसे आप जुडे।</string> + <string name="backup_type_search_relays">खोज पुनःप्रसारक सूची</string> + <string name="backup_type_search_relays_explainer">उपयुक्त पुनःप्रसारक जब आप खोजते हैं।</string> + <string name="backup_type_trust_providers">विश्वाससेवा प्रदाता सूची</string> + <string name="backup_type_trust_providers_explainer">सेवाएँ जिनपर आप विश्वास करते है आपके लिए लोगों का सत्यापन तथा श्रेणीकरण करने के लिए।</string> + <string name="backup_type_trusted_relays">विश्वसनीय पुनःप्रसारक सूची</string> + <string name="backup_type_trusted_relays_explainer">पुनःप्रसारक जिनपर आप पर्याप्त विश्वास करते हैं बिना पूछे उनसे संयोजन करने के लिए।</string> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml b/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml index 950cdfa8b9..4931f70f61 100644 --- a/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml @@ -5287,4 +5287,322 @@ <string name="cyberspace_bag_unsigned">Aláírás nélküli, így ez a szerző csak állítás</string> <string name="cyberspace_bag_unknown_kind">Egy %1$d típusú elem, amelyet ez a kliens nem jelenít meg</string> <string name="cyberspace_bag_opaque">%1$d bájt valamiből, amit ez a kliens nem olvas</string> + <string name="backup_action_block_again">%1$s újbóli letiltása</string> + <string name="backup_action_keep_count">%1$s megtartása</string> + <string name="backup_action_rejoin">Újbóli csatlakozás ehhez: %1$s</string> + <string name="backup_action_remute">%1$s újbóli némítása</string> + <string name="backup_action_restore_count">%1$s visszaállítása</string> + <string name="backup_action_restore_feeds">Hírfolyamok visszaállítása</string> + <string name="backup_action_restore_places">Helyek visszaállítása</string> + <string name="backup_action_restore_profile">Profil visszaállítása</string> + <string name="backup_action_restore_providers">Szolgáltatók visszaállítása</string> + <string name="backup_action_restore_relays">Átjátszók visszaállítása</string> + <string name="backup_action_restore_topics">Témák visszaállítása</string> + <string name="backup_action_restore_wallet">Pénztárca visszaállítása</string> + <string name="backup_card_accept">Elfogadás</string> + <plurals name="backup_card_and_added"> + <item quantity="one">%1$s és %2$d hozzáadva</item> + <item quantity="other">%1$s és %2$d hozzáadva</item> + </plurals> + <plurals name="backup_card_and_blocked"> + <item quantity="one">%1$s és %2$d letiltva</item> + <item quantity="other">%1$s és %2$d letiltva</item> + </plurals> + <plurals name="backup_card_and_followed"> + <item quantity="one">%1$s és %2$d követve</item> + <item quantity="other">%1$s és %2$d követve</item> + </plurals> + <plurals name="backup_card_and_joined"> + <item quantity="one">%1$s és %2$d csatlakozva</item> + <item quantity="other">%1$s és %2$d csatlakozva</item> + </plurals> + <plurals name="backup_card_and_muted"> + <item quantity="one">%1$s és %2$d némítva</item> + <item quantity="other">%1$s és %2$d némítva</item> + </plurals> + <string name="backup_card_app_settings">A szinkronizált Amethyst alkalmazásbeállításai egy másik, nem Amethyst alkalmazásban módosultak. Ez problémákat okozhat.</string> + <string name="backup_card_concord_emptied">Ön kiürítette a Concord listáját</string> + <string name="backup_card_concord_rewritten">Ön módosította a Concord listáját</string> + <plurals name="backup_card_dm_relays_lost"> + <item quantity="one">Ön %1$d privát üzenetátjátszót távolított el</item> + <item quantity="other">%1$d privát üzenet átjátszót távolított el</item> + </plurals> + <plurals name="backup_card_feeds_removed"> + <item quantity="one">Ön %1$d kedvenc hírfolyamot távolított el</item> + <item quantity="other">%1$d kedvenc hírfolyamot távolított el</item> + </plurals> + <plurals name="backup_card_follows_dropped"> + <item quantity="one">%1$d személyt nem követ többé</item> + <item quantity="other">%1$d személyt nem követ többé</item> + </plurals> + <plurals name="backup_card_hashtags_unfollowed"> + <item quantity="one">Ön %1$d kulcsszót nem követ többé</item> + <item quantity="other">%1$d hashtaget nem követ többé</item> + </plurals> + <plurals name="backup_card_indexer_relays_lost"> + <item quantity="one">Ön %1$d indexelő átjátszót távolított el</item> + <item quantity="other">%1$d indexelő átjátszót távolított el</item> + </plurals> + <plurals name="backup_card_key_package_relays_lost"> + <item quantity="one">Ön %1$d csoportos csevegési kulcsátjátszót távolított el</item> + <item quantity="other">%1$d csoportcsevegés kulcs átjátszót távolított el</item> + </plurals> + <plurals name="backup_card_leave_chats"> + <item quantity="one">Ön %1$d nyilvános csevegést hagyott el</item> + <item quantity="other">%1$d nyilvános csevegést hagyott el</item> + </plurals> + <plurals name="backup_card_leave_communities"> + <item quantity="one">Ön %1$d közösséget hagyott el</item> + <item quantity="other">%1$d közösséget hagyott el</item> + </plurals> + <plurals name="backup_card_leave_groups"> + <item quantity="one">Ön %1$d csoportot hagyott el</item> + <item quantity="other">%1$d csoportot hagyott el</item> + </plurals> + <plurals name="backup_card_leave_rooms"> + <item quantity="one">Ön %1$d csevegőszobát hagyott el</item> + <item quantity="other">%1$d csevegőszobát hagyott el</item> + </plurals> + <plurals name="backup_card_mutes_visible"> + <item quantity="one">Ön %1$d dolog némítását oldotta fel</item> + <item quantity="other">%1$d dolog némítását oldotta fel</item> + </plurals> + <plurals name="backup_card_nip65_lost"> + <item quantity="one">Ön %1$d nyilvános átjátszót távolított el</item> + <item quantity="other">%1$d nyilvános átjátszót távolított el</item> + </plurals> + <string name="backup_card_nutzap_key_removed">Ön eltávolította a nutzap kulcsát</string> + <string name="backup_card_nutzap_key_replaced">Ön lecserélte a nutzap kulcsát</string> + <plurals name="backup_card_nutzap_mints_removed"> + <item quantity="one">Ön %1$d nutzap pénzverdét távolított el</item> + <item quantity="other">%1$d nutzap pénzverdét távolított el</item> + </plurals> + <plurals name="backup_card_offers_removed"> + <item quantity="one">Ön %1$d BOLT12 ajánlatot távolított el</item> + <item quantity="other">%1$d BOLT12 ajánlatot távolított el</item> + </plurals> + <plurals name="backup_card_payment_targets_removed"> + <item quantity="one">Ön %1$d fizetési módot távolított el</item> + <item quantity="other">%1$d fizetési módot távolított el</item> + </plurals> + <plurals name="backup_card_places_unfollowed"> + <item quantity="one">Ön %1$d helyet nem követ többé</item> + <item quantity="other">%1$d helyet nem követ többé</item> + </plurals> + <plurals name="backup_card_private_outbox_lost"> + <item quantity="one">Ön %1$d privát átjátszót távolított el</item> + <item quantity="other">%1$d privát átjátszót távolított el</item> + </plurals> + <plurals name="backup_card_profile_lost_fields"> + <item quantity="one">Ön %1$d profilmezőt távolított el</item> + <item quantity="other">%1$d profilmezőt távolított el</item> + </plurals> + <plurals name="backup_card_relay_feeds_lost"> + <item quantity="one">Ön %1$d átjátszó-hírfolyamot távolított el</item> + <item quantity="other">%1$d átjátszó hírfolyamot távolított el</item> + </plurals> + <plurals name="backup_card_search_relays_lost"> + <item quantity="one">Ön %1$d kereső átjátszót távolított el</item> + <item quantity="other">%1$d kereső átjátszót távolított el</item> + </plurals> + <plurals name="backup_card_trust_services_changed"> + <item quantity="one">Ön %1$d bizalmi szolgáltatás szolgáltatóját módosította</item> + <item quantity="other">%1$d bizalmi szolgáltatás szolgáltatóját változtatta meg</item> + </plurals> + <plurals name="backup_card_trusted_lost"> + <item quantity="one">Ön %1$d átjátszóban nem bízik többé</item> + <item quantity="other">%1$d átjátszóban nem bízik többé</item> + </plurals> + <plurals name="backup_card_unblocked"> + <item quantity="one">Ön %1$d átjátszó letiltását oldotta fel</item> + <item quantity="other">%1$d átjátszó blokkolását oldotta fel</item> + </plurals> + <string name="backup_card_wallet_emptied">Ön kiürítette a Cashu pénztárcáját</string> + <string name="backup_card_wallet_rewritten">Ön módosította a Cashu pénztárcáját</string> + <string name="backup_conflict_added">Hozzáadva (%1$s)</string> + <string name="backup_conflict_changed">Módosítva (%1$s)</string> + <string name="backup_conflict_count_added">%1$s hozzáadva</string> + <string name="backup_conflict_count_changed">%1$s módosítva</string> + <string name="backup_conflict_count_removed">%1$s eltávolítva</string> + <string name="backup_conflict_keep_new">Új verzió használata</string> + <string name="backup_conflict_private_added">Ön privát (titkosított) elemeket adott hozzá.</string> + <string name="backup_conflict_private_changed">Ön privát (titkosított) elemeket is módosított. Ezek itt nem hasonlíthatók össze.</string> + <string name="backup_conflict_private_cleared">Ön eltávolította az összes privát (titkosított) elemet.</string> + <string name="backup_conflict_relay_read_only">csak olvasható</string> + <string name="backup_conflict_relay_read_write">olvasás és írás</string> + <string name="backup_conflict_relay_write_only">csak írható</string> + <string name="backup_conflict_removed">Eltávolítva (%1$s)</string> + <string name="backup_conflict_restore_mine">Mentett verzió visszaállítása</string> + <string name="backup_conflict_title">Ön módosította a következőt: %1$s, egy másik alkalmazásban</string> + <string name="backup_entry_algo_feed">Hírfolyamok</string> + <string name="backup_entry_chat_room">Csevegőszobák</string> + <string name="backup_entry_community">Közösségek</string> + <string name="backup_entry_hashtag">Kulcsszavak</string> + <string name="backup_entry_identity_claim">Kapcsolt identitások</string> + <string name="backup_entry_location">Helyek</string> + <string name="backup_entry_mint">Pénzverdék</string> + <string name="backup_entry_nutzap_key">Nutzap kulcsok</string> + <string name="backup_entry_offer">Ajánlatok</string> + <string name="backup_entry_other">Egyéb bejegyzések</string> + <string name="backup_entry_payment_target">Fizetési címek</string> + <string name="backup_entry_person">Személyek</string> + <string name="backup_entry_profile_field">Profilmezők</string> + <string name="backup_entry_public_chat">Nyilvános csevegések</string> + <string name="backup_entry_relay">Átjátszók</string> + <string name="backup_entry_relay_group">Csoportok</string> + <string name="backup_entry_thread">Beszélgetések</string> + <string name="backup_entry_trust_provider">Bizalmi szolgáltatók</string> + <string name="backup_entry_word">Szavak</string> + <string name="backup_profile_field_about">Névjegy</string> + <string name="backup_profile_field_banner">Borító</string> + <string name="backup_profile_field_birthday">Születésnap</string> + <string name="backup_profile_field_bot">Bot jelölő</string> + <string name="backup_profile_field_clink_offer">CLINK-ajánlat</string> + <string name="backup_profile_field_display_name">Megjelenített név</string> + <string name="backup_profile_field_lud06">LNURL</string> + <string name="backup_profile_field_lud16">Lightning-cím</string> + <string name="backup_profile_field_name">Felhasználónév</string> + <string name="backup_profile_field_nip05">Nostr-cím</string> + <string name="backup_profile_field_picture">Kép</string> + <string name="backup_profile_field_pronouns">Névmások</string> + <string name="backup_profile_field_website">Weboldal</string> + <string name="backup_review_allowed_tag">Engedélyezve</string> + <string name="backup_review_changed_by_other_app">Módosítva egy másik alkalmazásban · %1$s</string> + <string name="backup_review_concord_body">A Concord közösségei Önhöz vannak titkosítva, így itt nem hasonlíthatók össze. A visszaállítás pontosan azt hozza vissza, amit ez az eszköz mentett.</string> + <string name="backup_review_demoted_tag">Áthelyezve</string> + <string name="backup_review_dropped_count">%1$s elvetve</string> + <string name="backup_review_dropped_tag">Elvetve</string> + <string name="backup_review_encrypted_added">Ön hozta létre</string> + <string name="backup_review_encrypted_changed">Ön módosította</string> + <string name="backup_review_encrypted_cleared">Ön kiürítette</string> + <string name="backup_review_feed">Hírfolyam</string> + <string name="backup_review_gone">Eltűnt</string> + <string name="backup_review_hashtags_you_follow">Ön által követett kulcsszavak</string> + <string name="backup_review_inbox">Bejövő</string> + <string name="backup_review_inbox_explainer">ahol mások elérik Önt</string> + <string name="backup_review_joined">csatlakozott</string> + <string name="backup_review_kept">megtartva</string> + <string name="backup_review_kept_count">%1$s megtartva</string> + <string name="backup_review_key_removed">Fogadási kulcs eltávolítva</string> + <string name="backup_review_key_replaced">Fogadási kulcs lecserélve</string> + <string name="backup_review_key_warning">A nutzapok ehhez az új kulcshoz lesznek kötve. Ha ez az eszköz nem rendelkezik vele, lehet, hogy itt nem tudja beváltani őket.</string> + <string name="backup_review_left">elhagyta a csoportot</string> + <string name="backup_review_left_tag">Elhagyva</string> + <string name="backup_review_listed_tag">Listázva</string> + <string name="backup_review_moved_tag">Áthelyezve</string> + <string name="backup_review_names_and_more">%1$s és %2$s további</string> + <string name="backup_review_new">Új verzió</string> + <string name="backup_review_new_count">%1$s új</string> + <string name="backup_review_new_count_caption">új</string> + <string name="backup_review_new_tag">Új</string> + <string name="backup_review_newly_blocked">Újonnan letiltva</string> + <string name="backup_review_newly_muted">%1$s újonnan némítva</string> + <string name="backup_review_no_longer_blocked">Már nincs letiltva</string> + <string name="backup_review_not_set">Nincs beállítva</string> + <string name="backup_review_nothing_here">Itt nincs semmi</string> + <string name="backup_review_offer">BOLT12 ajánlat</string> + <plurals name="backup_review_offers_gone"> + <item quantity="one">%1$d ajánlat eltávolítva</item> + <item quantity="other">%1$d ajánlat eltávolítva</item> + </plurals> + <string name="backup_review_outbox">Kimenő</string> + <string name="backup_review_outbox_explainer">ahol közzétesz</string> + <plurals name="backup_review_payment_targets_gone"> + <item quantity="one">%1$d fizetési mód eltávolítva</item> + <item quantity="other">%1$d fizetési mód eltávolítva</item> + </plurals> + <string name="backup_review_payments_explainer">A fizető emberek csak azt látják, ami ebben a listában van.</string> + <string name="backup_review_places_gone">eltűnt helyek</string> + <string name="backup_review_places_you_follow">Ön által követett helyek</string> + <string name="backup_review_private_added_body">Most már titkosítva vannak, valószínűleg ide kerültek a fenti nyilvános elemek.</string> + <string name="backup_review_private_added_title">Ön priváttá tette az elemeket</string> + <string name="backup_review_private_changed_body">Titkosítva vannak, így itt nem hasonlíthatók össze.</string> + <string name="backup_review_private_changed_title">Ön módosította a privát elemeket</string> + <string name="backup_review_private_wiped_body">Titkosítva vannak, így csak Ön láthatja, mik voltak. A visszaállítás mindet visszahozza.</string> + <string name="backup_review_private_wiped_title">Ön eltávolította az összes privát elemet</string> + <string name="backup_review_receiving_tag">Fogadás</string> + <string name="backup_review_renamed">átnevezte</string> + <string name="backup_review_renamed_tag">Átnevezve</string> + <string name="backup_review_saved">Ezen az eszközön</string> + <string name="backup_review_search_people">%1$s személy keresése</string> + <string name="backup_review_searching_tag">Keresés</string> + <string name="backup_review_service">Szolgáltatás</string> + <string name="backup_review_service_followers">Követők</string> + <string name="backup_review_service_rank">Rang</string> + <string name="backup_review_service_topics">Témák</string> + <string name="backup_review_services_changed">%2$s szolgáltatásból %1$s módosult</string> + <string name="backup_review_services_changed_explainer">Most már valaki mástól vagy senkitől sem származnak</string> + <string name="backup_review_still_blocked">Továbbra is le van tiltva</string> + <string name="backup_review_storing_tag">Tárolás</string> + <string name="backup_review_tab_dropped">Elvetve · %1$s</string> + <string name="backup_review_tab_edited">Szerkesztve · %1$s</string> + <string name="backup_review_tab_new">Új · %1$s</string> + <string name="backup_review_title_cleared">%1$s törölve</string> + <string name="backup_review_title_updated">%1$s frissítve</string> + <string name="backup_review_topics_gone">eltűnt témák</string> + <string name="backup_review_trust_warning">A látott rangok, követőszámok és a kéretlen tartalomszűrés ezektől a szolgáltatóktól származnak.</string> + <string name="backup_review_trusted_tag">Megbízható</string> + <string name="backup_review_unblocked">%1$s letiltása feloldva</string> + <string name="backup_review_unblocked_explainer">Az Amethyst újra kapcsolódni fog hozzájuk</string> + <string name="backup_review_units_tag">Egységek</string> + <string name="backup_review_unmuted">%1$s némítása feloldva</string> + <string name="backup_review_unmuted_explainer">Ezek újra megjelenhetnek a hírfolyamaiban</string> + <string name="backup_review_untrusted_tag">Nem megbízható</string> + <string name="backup_review_wallet_body">A saját pénztárca pénzverdéi és kulcsai Önhöz vannak titkosítva, így itt nem hasonlíthatók össze tételesen. A visszaállítás pontosan azt hozza vissza, amit ez az eszköz mentett.</string> + <string name="backup_review_was_named">„%1$s” volt</string> + <string name="backup_review_words_and_hashtags">Szavak és kulcsszavak</string> + <string name="backup_type_app_settings">szinkronizált alkalmazásbeállítások</string> + <string name="backup_type_app_settings_explainer">Az eszközök között szinkronizált Amethyst-beállítások. Titkosítva vannak, így csak a jelenlétük hasonlítható össze.</string> + <string name="backup_type_blocked_relays">letiltott átjátszólista</string> + <string name="backup_type_blocked_relays_explainer">Átjátszók, amelyekhez ez az alkalmazás soha nem kapcsolódik.</string> + <string name="backup_type_bolt12_offers">BOLT12 ajánlatok</string> + <string name="backup_type_bolt12_offers_explainer">A lightning ajánlatok, amelyeket mások a fizetéshez használnak az Ön számára.</string> + <string name="backup_type_cashu_wallet">Cashu pénztárca</string> + <string name="backup_type_cashu_wallet_explainer">Az ecash pénztárca beállításai és pénzverdéi. Titkosítva vannak, így csak a jelenlétük hasonlítható össze.</string> + <string name="backup_type_communities">közösséglista</string> + <string name="backup_type_communities_explainer">Közösségek, amelyekhez csatlakozott.</string> + <string name="backup_type_concord_communities">Concord közösséglista</string> + <string name="backup_type_concord_communities_explainer">A Concord közösségek, amelyekhez csatlakozott. Titkosítva vannak, így csak a jelenlétük hasonlítható össze.</string> + <string name="backup_type_dm_relays">privát üzenet átjátszólista</string> + <string name="backup_type_dm_relays_explainer">Ahová mások kézbesítik a privát üzeneteit.</string> + <string name="backup_type_ephemeral_chats">rövid életű csevegési lista</string> + <string name="backup_type_ephemeral_chats_explainer">Rövid életű csevegőszobák, amelyekhez csatlakozott.</string> + <string name="backup_type_favorite_algo_feeds">kedvenc hírfolyamlista</string> + <string name="backup_type_favorite_algo_feeds_explainer">Mentett algoritmus-hírfolyamok.</string> + <string name="backup_type_follow_list">követési lista</string> + <string name="backup_type_follow_list_explainer">Meghatározza, kinek a bejegyzései jelenjenek meg a kezdőlapon.</string> + <string name="backup_type_geohashes">követett helyek</string> + <string name="backup_type_geohashes_explainer">Helyek, amelyek bejegyzései megjelennek a hírfolyamaiban.</string> + <string name="backup_type_hashtags">követett kulcsszavak</string> + <string name="backup_type_hashtags_explainer">Kulcsszavak, amelyek bejegyzései megjelennek a hírfolyamban.</string> + <string name="backup_type_indexer_relays">indexelő átjátszólista</string> + <string name="backup_type_indexer_relays_explainer">Azok az átjátszók, amelyeket mások profiljainak és átjátszólistáinak megtalálásához használnak.</string> + <string name="backup_type_key_package_relays">csoportos csevegési kulcsátjátszók</string> + <string name="backup_type_key_package_relays_explainer">Ahol mások megtalálják a kulcsokat, hogy titkosított csoportos csevegésekhez adhassák Önt.</string> + <string name="backup_type_mute_list">némítási lista</string> + <string name="backup_type_mute_list_explainer">Elrejti a személyeket, szavakat, kulcsszavakat és beszélgetéseket mindenből, amit lát.</string> + <string name="backup_type_nutzap_info">nutzap fogadási beállítások</string> + <string name="backup_type_nutzap_info_explainer">A pénzverdék és a kulcs, amelyeket mások a nutzapok küldéséhez használnak az Ön számára.</string> + <string name="backup_type_other">lista</string> + <string name="backup_type_other_explainer">Egy listaelem ezen az eszközön mentve.</string> + <string name="backup_type_outbox_inbox_relays">nyilvános átjátszólista</string> + <string name="backup_type_outbox_inbox_relays_explainer">Megmondja mindenkinek, hol találhatók a bejegyzései, és hová küldjék a válaszokat és említéseket.</string> + <string name="backup_type_payment_targets">fizetési címek</string> + <string name="backup_type_payment_targets_explainer">A módok, ahogy mások fizethetnek Önnek.</string> + <string name="backup_type_private_outbox_relays">privát átjátszólista</string> + <string name="backup_type_private_outbox_relays_explainer">Ahol a piszkozatai és privát adatai tárolódnak.</string> + <string name="backup_type_profile">profil</string> + <string name="backup_type_profile_explainer">Az Ön neve, képe, bemutatkozása és fizetési címei, ahogy mindenki látja őket.</string> + <string name="backup_type_public_chats">nyilvános csevegési lista</string> + <string name="backup_type_public_chats_explainer">Nyilvános csevegések, amelyekhez csatlakozott.</string> + <string name="backup_type_relay_feeds">kedvenc átjátszóhírfolyamok</string> + <string name="backup_type_relay_feeds_explainer">Olyan átjátszók, amelyeket hírfolyamként böngész.</string> + <string name="backup_type_relay_groups">csoportlista</string> + <string name="backup_type_relay_groups_explainer">Átjátszó-alapú csoportok, amelyekhez csatlakozott.</string> + <string name="backup_type_search_relays">kereső átjátszólista</string> + <string name="backup_type_search_relays_explainer">Kereséskor használt átjátszók.</string> + <string name="backup_type_trust_providers">bizalmi szolgáltatólista</string> + <string name="backup_type_trust_providers_explainer">A szolgáltatások, amelyekben megbízik, hogy rangsorolják és ellenőrizzék Önnek az embereket.</string> + <string name="backup_type_trusted_relays">megbízható átjátszólista</string> + <string name="backup_type_trusted_relays_explainer">Átjátszók, amelyekhez kérdezés nélkül kapcsolódhat.</string> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-pl-rPL/strings.xml b/commonsUI/src/commonMain/composeResources/values-pl-rPL/strings.xml index 55a0998df3..6b3fd3bf9a 100644 --- a/commonsUI/src/commonMain/composeResources/values-pl-rPL/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-pl-rPL/strings.xml @@ -5507,4 +5507,382 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest <string name="cyberspace_bag_unsigned">Brak podpisu, więc autor to tylko domysł</string> <string name="cyberspace_bag_unknown_kind">Element typu %1$d, którego ten klient nie wyświetla</string> <string name="cyberspace_bag_opaque">%1$d bajtów danych, których ten klient nie odczytuje</string> + <string name="backup_action_block_again">Zablokuj %1$s ponownie</string> + <string name="backup_action_keep_count">Zachowaj %1$s</string> + <string name="backup_action_rejoin">Ponów %1$s</string> + <string name="backup_action_remute">Ponownie wycisz %1$s</string> + <string name="backup_action_restore_count">Przywróć %1$s</string> + <string name="backup_action_restore_feeds">Przywróć kanały</string> + <string name="backup_action_restore_places">Przywróć miejsca</string> + <string name="backup_action_restore_profile">Przywróć profil</string> + <string name="backup_action_restore_providers">Przywróć dostawców</string> + <string name="backup_action_restore_relays">Przywróć transmitery</string> + <string name="backup_action_restore_topics">Przywróć tematy</string> + <string name="backup_action_restore_wallet">Przywróć portfel</string> + <string name="backup_card_accept">Zaakceptuj</string> + <plurals name="backup_card_and_added"> + <item quantity="one">%1$s i dodano %2$d</item> + <item quantity="few">%1$s i dodano %2$d</item> + <item quantity="many">%1$s i dodano %2$d</item> + <item quantity="other">%1$s i dodano %2$d</item> + </plurals> + <plurals name="backup_card_and_blocked"> + <item quantity="one">%1$s i zablokowano %2$d</item> + <item quantity="few">%1$s i zablokowano %2$d</item> + <item quantity="many">%1$s i zablokowano %2$d</item> + <item quantity="other">%1$s i zablokowano %2$d</item> + </plurals> + <plurals name="backup_card_and_followed"> + <item quantity="one">%1$s i obserwowany %2$d</item> + <item quantity="few">%1$s i obserwowanych %2$d</item> + <item quantity="many">%1$s i obserwowanych %2$d</item> + <item quantity="other">%1$s i obserwowane %2$d</item> + </plurals> + <plurals name="backup_card_and_joined"> + <item quantity="one">%1$s i dołączono do %2$d</item> + <item quantity="few">%1$s i dołączono do %2$d</item> + <item quantity="many">%1$s i dołączono do %2$d</item> + <item quantity="other">%1$s i dołączono do %2$d</item> + </plurals> + <plurals name="backup_card_and_muted"> + <item quantity="one">%1$s i wyciszony %2$d</item> + <item quantity="few">%1$s i wyciszono %2$d</item> + <item quantity="many">%1$s i wyciszono %2$d</item> + <item quantity="other">%1$s i wyciszone %2$d</item> + </plurals> + <string name="backup_card_app_settings">Twoje zsynchronizowane ustawienia aplikacji Amethyst zostały zmienione w innej aplikacji, która nie jest Amethystem. To może powodować problemy.</string> + <string name="backup_card_concord_emptied">Opróżniłeś swoją listę Concorda</string> + <string name="backup_card_concord_rewritten">Zmieniłeś swoją listę Concorda</string> + <plurals name="backup_card_dm_relays_lost"> + <item quantity="one">Usunąłeś %1$d transmiter DM</item> + <item quantity="few">Usunąłeś %1$d transmiterów DM</item> + <item quantity="many">Usunąłeś %1$d transmiterów DM</item> + <item quantity="other">Usunąłeś %1$d transmitery DM</item> + </plurals> + <plurals name="backup_card_feeds_removed"> + <item quantity="one">Usunąłeś %1$d ulubiony kanał</item> + <item quantity="few">Usunąłeś %1$d ulubionych kanałów</item> + <item quantity="many">Usunąłeś %1$d ulubionych kanałów</item> + <item quantity="other">Usunięto %1$d ulubione kanały</item> + </plurals> + <plurals name="backup_card_follows_dropped"> + <item quantity="one">Przestałeś obserwować %1$d osobę</item> + <item quantity="few">Przestałeś obserwować %1$d osób</item> + <item quantity="many">Przestałeś obserwować %1$d osób</item> + <item quantity="other">Przestałeś obserwować %1$d osoby</item> + </plurals> + <plurals name="backup_card_hashtags_unfollowed"> + <item quantity="one">Przestałeś obserwować %1$d hashtag</item> + <item quantity="few">Przestałeś obserwować %1$d hashtagów</item> + <item quantity="many">Przestałeś obserwować %1$d hashtagów</item> + <item quantity="other">Przestałeś obserwować %1$d hashtagi</item> + </plurals> + <plurals name="backup_card_indexer_relays_lost"> + <item quantity="one">Usunięto %1$d transmiter indeksujący</item> + <item quantity="few">Usunięto %1$d transmiterów indeksujących</item> + <item quantity="many">Usunięto %1$d transmiterów indeksujących</item> + <item quantity="other">Usunięto %1$d transmitery indeksujące</item> + </plurals> + <plurals name="backup_card_key_package_relays_lost"> + <item quantity="one">Usunięto %1$d transmiter klucza grupowego czatu</item> + <item quantity="few">Usunięto %1$d transmiterów klucza grupowego czatu</item> + <item quantity="many">Usunięto %1$d transmiterów klucza grupowego czatu</item> + <item quantity="other">Usunięto %1$d transmitery klucza grupowego czatu</item> + </plurals> + <plurals name="backup_card_leave_chats"> + <item quantity="one">Opuściłeś %1$d czat publiczny</item> + <item quantity="few">Opuściłeś %1$d czatów publicznych</item> + <item quantity="many">Opuściłeś %1$d czatów publicznych</item> + <item quantity="other">Opuściłeś %1$d czaty publiczne</item> + </plurals> + <plurals name="backup_card_leave_communities"> + <item quantity="one">Opuściłeś %1$d społeczność</item> + <item quantity="few">Opuściłeś %1$d społeczności</item> + <item quantity="many">Opuściłeś %1$d społeczności</item> + <item quantity="other">Opuściłeś %1$d społeczności</item> + </plurals> + <plurals name="backup_card_leave_groups"> + <item quantity="one">Opuściłeś %1$d grupę</item> + <item quantity="few">Opuściłeś %1$d grup</item> + <item quantity="many">Opuściłeś %1$d grup</item> + <item quantity="other">Opuszczono %1$d grupy</item> + </plurals> + <plurals name="backup_card_leave_rooms"> + <item quantity="one">Opuściłeś %1$d pokój czatu</item> + <item quantity="few">Opuściłeś %1$d pokoi czatu</item> + <item quantity="many">Opuściłeś %1$d pokoi czatu</item> + <item quantity="other">Opuściłeś %1$d pokoje czatu</item> + </plurals> + <plurals name="backup_card_mutes_visible"> + <item quantity="one">Włączyłeś dźwięk dla %1$d elementu</item> + <item quantity="few">Włączyłeś dźwięk dla %1$d elementów</item> + <item quantity="many">Włączyłeś dźwięk dla %1$d elementów</item> + <item quantity="other">%1$d elementy mają włączony dźwięk</item> + </plurals> + <plurals name="backup_card_nip65_lost"> + <item quantity="one">Usunąłeś %1$d publiczny transmiter</item> + <item quantity="few">Usunąłeś %1$d publicznych transmiterów</item> + <item quantity="many">Usunąłeś %1$d publicznych transmiterów</item> + <item quantity="other">Usunąłeś %1$d publiczne transmitery</item> + </plurals> + <string name="backup_card_nutzap_key_removed">Usunąłeś swój klucz nutzap</string> + <string name="backup_card_nutzap_key_replaced">Wymieniłeś swój klucz Nutzap</string> + <plurals name="backup_card_nutzap_mints_removed"> + <item quantity="one">Usunięto %1$d nutzap mint</item> + <item quantity="few">Usunięto %1$d nutzap mintów</item> + <item quantity="many">Usunięto %1$d nutzap mintów</item> + <item quantity="other">Usunięto %1$d nutzap minty</item> + </plurals> + <plurals name="backup_card_offers_removed"> + <item quantity="one">Usunięto %1$d ofertę BOLT12</item> + <item quantity="few">Usunięto %1$d ofert BOLT12</item> + <item quantity="many">Usunięto %1$d ofert BOLT12</item> + <item quantity="other">Usunięto %1$d oferty BOLT12</item> + </plurals> + <plurals name="backup_card_payment_targets_removed"> + <item quantity="one">Usunięto %1$d sposób na otrzymanie zapłaty</item> + <item quantity="few">Usunięto %1$d sposobów otrzymywania zapłaty</item> + <item quantity="many">Usunięto %1$d sposobów otrzymywania zapłaty</item> + <item quantity="other">Usunięto %1$d sposoby otrzymywania zapłaty</item> + </plurals> + <plurals name="backup_card_places_unfollowed"> + <item quantity="one">Przestałeś obserwować %1$d miejsce</item> + <item quantity="few">Przestałeś obserwować %1$d miejsc</item> + <item quantity="many">Przestałeś obserwować %1$d miejsc</item> + <item quantity="other">Przestałeś obserwować %1$d miejsca</item> + </plurals> + <plurals name="backup_card_private_outbox_lost"> + <item quantity="one">Usunięto %1$d prywatny transmiter</item> + <item quantity="few">Usunięto %1$d prywatnych transmiterów</item> + <item quantity="many">Usunięto %1$d prywatnych transmiterów</item> + <item quantity="other">Usunięto %1$d prywatne transmitery</item> + </plurals> + <plurals name="backup_card_profile_lost_fields"> + <item quantity="one">Usunąłeś %1$d pole profilu</item> + <item quantity="few">Usunąłeś %1$d pól profilowych</item> + <item quantity="many">Usunąłeś %1$d pól profilowych</item> + <item quantity="other">Usunąłeś %1$d pola profilowe</item> + </plurals> + <plurals name="backup_card_relay_feeds_lost"> + <item quantity="one">Usunięto %1$d kanał transmitera</item> + <item quantity="few">Usunięto %1$d kanałów transmitera</item> + <item quantity="many">Usunięto %1$d kanałów transmitera</item> + <item quantity="other">Usunięto %1$d kanały transmitera</item> + </plurals> + <plurals name="backup_card_search_relays_lost"> + <item quantity="one">Usunięto %1$d transmiter wyszukiwania</item> + <item quantity="few">Usunięto %1$d transmiterów wyszukiwania</item> + <item quantity="many">Usunięto %1$d transmiterów wyszukiwania</item> + <item quantity="other">Usunięto %1$d transmitery wyszukiwania</item> + </plurals> + <plurals name="backup_card_trust_services_changed"> + <item quantity="one">Zmieniłeś dostawcę %1$d zaufanego serwisu</item> + <item quantity="few">Zmieniłeś dostawcę %1$d zaufanych serwisów</item> + <item quantity="many">Zmieniłeś dostawcę %1$d zaufanych serwisów</item> + <item quantity="other">Zmieniłeś dostawcę %1$d zaufanych serwisów</item> + </plurals> + <plurals name="backup_card_trusted_lost"> + <item quantity="one">Przestałeś ufać %1$d transmiterowi</item> + <item quantity="few">Przestałeś ufać %1$d transmiterom</item> + <item quantity="many">Przestałeś ufać %1$d transmiterom</item> + <item quantity="other">Przestałeś ufać %1$d transmiterom</item> + </plurals> + <plurals name="backup_card_unblocked"> + <item quantity="one">Odblokowałeś %1$d transmiter</item> + <item quantity="few">Odblokowałeś %1$d transmiterów</item> + <item quantity="many">Odblokowałeś %1$d transmiterów</item> + <item quantity="other">Odblokowałeś %1$d transmitery</item> + </plurals> + <string name="backup_card_wallet_emptied">Opróżniłeś swój portfel Cashu</string> + <string name="backup_card_wallet_rewritten">Zmieniłeś swój portfel Cashu</string> + <string name="backup_conflict_added">Dodano (%1$s)</string> + <string name="backup_conflict_changed">Zmieniono (%1$s)</string> + <string name="backup_conflict_count_added">Dodano %1$s</string> + <string name="backup_conflict_count_changed">%1$s zmieniono</string> + <string name="backup_conflict_count_removed">Usunięto %1$s</string> + <string name="backup_conflict_keep_new">Użyj nowej wersji</string> + <string name="backup_conflict_private_added">Dodano prywatne (zaszyfrowane) pozycje.</string> + <string name="backup_conflict_private_changed">Zmieniłeś również elementy prywatne (zaszyfrowane). Nie można ich tutaj porównać.</string> + <string name="backup_conflict_private_cleared">Usunąłeś wszystkie prywatne (zaszyfrowane) elementy.</string> + <string name="backup_conflict_relay_read_only">tylko do odczytu</string> + <string name="backup_conflict_relay_read_write">odczyt i zapis</string> + <string name="backup_conflict_relay_write_only">tylko do zapisu</string> + <string name="backup_conflict_removed">Usunięto (%1$s)</string> + <string name="backup_conflict_restore_mine">Przywróć zapisaną wersję</string> + <string name="backup_conflict_title">Zmieniłeś %1$s w innej aplikacji</string> + <string name="backup_entry_algo_feed">Kanały</string> + <string name="backup_entry_chat_room">Czat pokoje</string> + <string name="backup_entry_community">Społeczności</string> + <string name="backup_entry_hashtag">Hashtagi</string> + <string name="backup_entry_identity_claim">Powiązane tożsamości</string> + <string name="backup_entry_location">Lokalizacje</string> + <string name="backup_entry_mint">Minty</string> + <string name="backup_entry_nutzap_key">Klucze Nutzap</string> + <string name="backup_entry_offer">Oferty</string> + <string name="backup_entry_other">Inne wpisy</string> + <string name="backup_entry_payment_target">Adresy płatności</string> + <string name="backup_entry_person">Osoby</string> + <string name="backup_entry_profile_field">Pola profilu</string> + <string name="backup_entry_public_chat">Czaty publiczne</string> + <string name="backup_entry_relay">Transmitery</string> + <string name="backup_entry_relay_group">Grupy</string> + <string name="backup_entry_thread">Wątki</string> + <string name="backup_entry_trust_provider">Zaufani dostawcy</string> + <string name="backup_entry_word">Słowa</string> + <string name="backup_profile_field_about">O profilu</string> + <string name="backup_profile_field_banner">Baner</string> + <string name="backup_profile_field_birthday">Data urodzin</string> + <string name="backup_profile_field_bot">Flaga bota</string> + <string name="backup_profile_field_clink_offer">Oferta CLINK</string> + <string name="backup_profile_field_display_name">Nazwa wyświetlana</string> + <string name="backup_profile_field_lud06">LNURL</string> + <string name="backup_profile_field_lud16">Lightning Adres</string> + <string name="backup_profile_field_name">Nazwa użytkownika</string> + <string name="backup_profile_field_nip05">Adres Nostr</string> + <string name="backup_profile_field_picture">Zdjęcie</string> + <string name="backup_profile_field_pronouns">Zaimki</string> + <string name="backup_profile_field_website">Strona www</string> + <string name="backup_review_allowed_tag">Dozwolona</string> + <string name="backup_review_changed_by_other_app">Zmieniono w innej aplikacji · %1$s</string> + <string name="backup_review_concord_body">Twoje społeczności w Concord są dla Ciebie zaszyfrowane, więc nie można ich tutaj porównać. Przywrócenie danych powoduje przywrócenie dokładnie tego, co zostało zapisane na tym urządzeniu.</string> + <string name="backup_review_demoted_tag">Przeniesiona</string> + <string name="backup_review_dropped_count">%1$s odrzuconych</string> + <string name="backup_review_dropped_tag">Usunięta</string> + <string name="backup_review_encrypted_added">Utworzyłeś to</string> + <string name="backup_review_encrypted_changed">Zmieniłeś to</string> + <string name="backup_review_encrypted_cleared">Opróżniłeś to</string> + <string name="backup_review_feed">Kanał</string> + <string name="backup_review_gone">Zniknęła</string> + <string name="backup_review_hashtags_you_follow">Hashtagi, które obserwujesz</string> + <string name="backup_review_inbox">Skrzynka odbiorcza</string> + <string name="backup_review_inbox_explainer">gdzie inni docierają do Ciebie</string> + <string name="backup_review_joined">dołączył(a)</string> + <string name="backup_review_kept">zachowane</string> + <string name="backup_review_kept_count">Zachowano %1$s</string> + <string name="backup_review_key_removed">Usunięto klucz odbierający</string> + <string name="backup_review_key_replaced">Wymieniono klucz odbierający</string> + <string name="backup_review_key_warning">Nutzapy zostaną przypisane do tego nowego klucza. Jeśli urządzenie nie posiada tego klucza, może się okazać, że nie będzie można ich tutaj zrealizować.</string> + <string name="backup_review_left">zostało</string> + <string name="backup_review_left_tag">Zostało</string> + <string name="backup_review_listed_tag">Na Liście</string> + <string name="backup_review_moved_tag">Przeniesiona</string> + <string name="backup_review_names_and_more">%1$s i %2$s więcej</string> + <string name="backup_review_new">Nowa wersja</string> + <string name="backup_review_new_count">%1$s nowych</string> + <string name="backup_review_new_count_caption">nowe</string> + <string name="backup_review_new_tag">Nowa</string> + <string name="backup_review_newly_blocked">Nowo zablokowana</string> + <string name="backup_review_newly_muted">%1$s nowo wyciszonych</string> + <string name="backup_review_no_longer_blocked">Nie jest już zablokowana</string> + <string name="backup_review_not_set">Nie ustawiono</string> + <string name="backup_review_nothing_here">Nic tu nie ma!</string> + <string name="backup_review_offer">Oferta BOLT12</string> + <plurals name="backup_review_offers_gone"> + <item quantity="one">%1$d oferta usunięta</item> + <item quantity="few">%1$d ofert usunięto</item> + <item quantity="many">%1$d ofert usunięto</item> + <item quantity="other">%1$d oferty usunięte</item> + </plurals> + <string name="backup_review_outbox">Skrzynka nadawcza</string> + <string name="backup_review_outbox_explainer">gdzie publikujesz</string> + <plurals name="backup_review_payment_targets_gone"> + <item quantity="one">%1$d sposób płatności został usunięty</item> + <item quantity="few">%1$d sposobów płatności zostało usuniętych</item> + <item quantity="many">%1$d sposobów płatności zostało usuniętych</item> + <item quantity="other">%1$d sposoby płatności zostały usunięte</item> + </plurals> + <string name="backup_review_payments_explainer">Osoby, które Ci płacą, widzą tylko to, co znajduje się na tej liście.</string> + <string name="backup_review_places_gone"> miejsca zniknęły</string> + <string name="backup_review_places_you_follow">Miejsca które obserwujesz</string> + <string name="backup_review_private_added_body">Są teraz zaszyfrowane, co prawdopodobnie wyjaśnia, gdzie podziały się powyższe elementy publiczne.</string> + <string name="backup_review_private_added_title">Ustawiłeś te pozycje jako prywatne</string> + <string name="backup_review_private_changed_body">Są zaszyfrowane, więc nie można ich tu porównać.</string> + <string name="backup_review_private_changed_title">Zmieniłeś prywatne elementy</string> + <string name="backup_review_private_wiped_body">Są zaszyfrowane, więc tylko Ty możesz zobaczyć, co to było. Odtworzenie przywróci je wszystkie.</string> + <string name="backup_review_private_wiped_title">Usunąłeś wszystkie prywatne elementy</string> + <string name="backup_review_receiving_tag">Pobieranie</string> + <string name="backup_review_renamed">zmieniono nazwę</string> + <string name="backup_review_renamed_tag">Zmieniono nazwę</string> + <string name="backup_review_saved">Na tym urządzeniu</string> + <string name="backup_review_search_people">Szukaj %1$s osób</string> + <string name="backup_review_searching_tag">Szukam</string> + <string name="backup_review_service">Serwis</string> + <string name="backup_review_service_followers">Obserwujący</string> + <string name="backup_review_service_rank">Ranking</string> + <string name="backup_review_service_topics">Tematy</string> + <string name="backup_review_services_changed">Zmieniono %1$s z %2$s serwisów</string> + <string name="backup_review_services_changed_explainer">Teraz pochodzą od kogoś innego albo od nikogo</string> + <string name="backup_review_still_blocked">Nadal zablokowana</string> + <string name="backup_review_storing_tag">Przechowywanie</string> + <string name="backup_review_tab_dropped">Usunięte · %1$s</string> + <string name="backup_review_tab_edited">Edytowane · %1$s</string> + <string name="backup_review_tab_new">Nowe · %1$s</string> + <string name="backup_review_title_cleared">%1$s wyczyszczono</string> + <string name="backup_review_title_updated">%1$s zaktualizowano</string> + <string name="backup_review_topics_gone">tematy zniknęły</string> + <string name="backup_review_trust_warning">Rankingi, liczba obserwujących i filtrowanie spamu, które widzisz, pochodzą od tych dostawców.</string> + <string name="backup_review_trusted_tag">Zaufane</string> + <string name="backup_review_unblocked">%1$s odblokowane</string> + <string name="backup_review_unblocked_explainer">Ametyst ponownie połączy się z nimi</string> + <string name="backup_review_units_tag">Jednostek</string> + <string name="backup_review_unmuted">%1$s odblokowanych</string> + <string name="backup_review_unmuted_explainer">Mogą one ponownie pojawić się w Twoich informacjach</string> + <string name="backup_review_untrusted_tag">Niezaufane</string> + <string name="backup_review_wallet_body">Minty i klucze w Twoim portfelu są zaszyfrowane dla Ciebie, więc nie można ich tutaj porównać pozycja po pozycji. Przywrócenie danych powoduje przywrócenie dokładnie tego, co zostało zapisane na tym urządzeniu.</string> + <string name="backup_review_was_named">było „%1$s”</string> + <string name="backup_review_words_and_hashtags">Wyrazy & hasztagi</string> + <string name="backup_type_app_settings">zsynchronizowane ustawienia aplikacji</string> + <string name="backup_type_app_settings_explainer">Ustawienia Amethyst są synchronizowane na wszystkich Twoich urządzeniach. Są one zaszyfrowane, więc można porównać jedynie sam fakt ich obecności.</string> + <string name="backup_type_blocked_relays">lista zablokowanych przekaźników</string> + <string name="backup_type_blocked_relays_explainer">Transmitery, z którymi ta aplikacja nigdy się nie łączy.</string> + <string name="backup_type_bolt12_offers">Oferta BOLT12</string> + <string name="backup_type_bolt12_offers_explainer">Ta możliwość pozwala innym płacić Ci za pomocą Lightning.</string> + <string name="backup_type_cashu_wallet">Portfel Cashu</string> + <string name="backup_type_cashu_wallet_explainer">Ustawienia portfela ecash oraz minty. Są one zaszyfrowane, więc można porównać jedynie sam fakt ich istnienia.</string> + <string name="backup_type_communities">lista społeczności</string> + <string name="backup_type_communities_explainer">Społeczności, do których dołączyłeś.</string> + <string name="backup_type_concord_communities">Lista społeczności Concord</string> + <string name="backup_type_concord_communities_explainer">Społeczności Concord, do których dołączyłeś. Są one zaszyfrowane, więc można porównać jedynie fakt ich istnienia.</string> + <string name="backup_type_dm_relays">Lista transmiterów DM</string> + <string name="backup_type_dm_relays_explainer">Gdzie inni dostarczają Twoje prywatne wiadomości.</string> + <string name="backup_type_ephemeral_chats">lista czatów efemerycznych</string> + <string name="backup_type_ephemeral_chats_explainer">Tymczasowe czaty, do których dołączyłeś.</string> + <string name="backup_type_favorite_algo_feeds">lista ulubionych kanałów</string> + <string name="backup_type_favorite_algo_feeds_explainer">Zapisane przez Ciebie kanały algorytmiczne.</string> + <string name="backup_type_follow_list">lista obserwowanych</string> + <string name="backup_type_follow_list_explainer">Określa, czyje posty pojawiają się w Twoim kanale głównym.</string> + <string name="backup_type_geohashes">obserwowane lokalizacje</string> + <string name="backup_type_geohashes_explainer">Miejsca, z których posty pojawiają się na Twoich kanałach.</string> + <string name="backup_type_hashtags">obserwowane hashtagi</string> + <string name="backup_type_hashtags_explainer">Hashtagi, z których pochodzą posty wyświetlane w Twoich kanałach.</string> + <string name="backup_type_indexer_relays">lista transmiterów indeksujących</string> + <string name="backup_type_indexer_relays_explainer">Transmitery służące do wyszukiwania profili innych osób oraz list transmiterów.</string> + <string name="backup_type_key_package_relays">transmitery kluczy czatu grupowego</string> + <string name="backup_type_key_package_relays_explainer">Gdzie inni znajdą klucze do dodawania cię do zaszyfrowanych czatów grupowych.</string> + <string name="backup_type_mute_list">lista zablokowanych</string> + <string name="backup_type_mute_list_explainer">Ukrywa osoby, słowa, hashtagi i wątki we wszystkich wyświetlanych treściach.</string> + <string name="backup_type_nutzap_info">ustawienia odbioru nutzap</string> + <string name="backup_type_nutzap_info_explainer">Minty i klucz, które inni używają do wysyłania ci „nutzapów”.</string> + <string name="backup_type_other">lista</string> + <string name="backup_type_other_explainer">Lista zapisana na tym urządzeniu.</string> + <string name="backup_type_outbox_inbox_relays">lista publicznych transmiterów</string> + <string name="backup_type_outbox_inbox_relays_explainer">Informuje wszystkich, gdzie mogą znaleźć Twoje posty oraz gdzie mają wysyłać Ci odpowiedzi i wzmianki.</string> + <string name="backup_type_payment_targets">adresy płatności</string> + <string name="backup_type_payment_targets_explainer">Sposoby, w jakie inni mogą Ci zapłacić.</string> + <string name="backup_type_private_outbox_relays">lista prywatnych transmiterów</string> + <string name="backup_type_private_outbox_relays_explainer">Gdzie przechowywane są Twoje projekty i prywatne dane.</string> + <string name="backup_type_profile">profil</string> + <string name="backup_type_profile_explainer">Twoje imię i nazwisko, zdjęcie, biografia oraz adresy do płatności – tak, jak widzą je wszyscy.</string> + <string name="backup_type_public_chats">lista czatów publicznych</string> + <string name="backup_type_public_chats_explainer">Publiczne czaty, do których dołączyłeś.</string> + <string name="backup_type_relay_feeds">ulubione kanały transmitera</string> + <string name="backup_type_relay_feeds_explainer">Transmitery, które przeglądasz jako kanały informacyjne.</string> + <string name="backup_type_relay_groups">lista grup</string> + <string name="backup_type_relay_groups_explainer">Grupy oparte na transmiterach, do których dołączyłeś.</string> + <string name="backup_type_search_relays">lista transmiterów wyszukiwania</string> + <string name="backup_type_search_relays_explainer">Transmitery używane podczas wyszukiwania.</string> + <string name="backup_type_trust_providers">lista zaufanych dostawców</string> + <string name="backup_type_trust_providers_explainer">Serwisy, którym ufasz, a które zajmują się klasyfikowaniem i weryfikacją osób w Twoim imieniu.</string> + <string name="backup_type_trusted_relays">lista zaufanych transmiterów</string> + <string name="backup_type_trusted_relays_explainer">Zaufane transmitery, z którymi bez wahania nawiązujesz połączenie.</string> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values/strings.xml b/commonsUI/src/commonMain/composeResources/values/strings.xml index a0b587b986..f8a7c986c4 100644 --- a/commonsUI/src/commonMain/composeResources/values/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values/strings.xml @@ -5482,4 +5482,574 @@ <string name="cyberspace_bag_unsigned">Unsigned, so this author is a claim</string> <string name="cyberspace_bag_unknown_kind">An item of kind %1$d, which this client does not draw</string> <string name="cyberspace_bag_opaque">%1$d bytes of something this client does not read</string> + <string name="backup_action_block_again">Block %1$s again</string> + <string name="backup_action_keep_count">Keep %1$s</string> + <string name="backup_action_rejoin">Rejoin %1$s</string> + <string name="backup_action_remute">Re-mute %1$s</string> + <string name="backup_action_restore_count">Restore %1$s</string> + <string name="backup_action_restore_feeds">Restore feeds</string> + <string name="backup_action_restore_places">Restore places</string> + <string name="backup_action_restore_profile">Restore profile</string> + <string name="backup_action_restore_providers">Restore providers</string> + <string name="backup_action_restore_relays">Restore relays</string> + <string name="backup_action_restore_topics">Restore topics</string> + <string name="backup_action_restore_wallet">Restore wallet</string> + <string name="backup_card_accept">Accept</string> + <plurals name="backup_card_and_added"> + <item quantity="one">%1$s and added %2$d</item> + <item quantity="other">%1$s and added %2$d</item> + </plurals> + <plurals name="backup_card_and_blocked"> + <item quantity="one">%1$s and blocked %2$d</item> + <item quantity="other">%1$s and blocked %2$d</item> + </plurals> + <plurals name="backup_card_and_followed"> + <item quantity="one">%1$s and followed %2$d</item> + <item quantity="other">%1$s and followed %2$d</item> + </plurals> + <plurals name="backup_card_and_joined"> + <item quantity="one">%1$s and joined %2$d</item> + <item quantity="other">%1$s and joined %2$d</item> + </plurals> + <plurals name="backup_card_and_muted"> + <item quantity="one">%1$s and muted %2$d</item> + <item quantity="other">%1$s and muted %2$d</item> + </plurals> + <string name="backup_card_app_settings">Your Amethyst synced app settings changed in another app that is not Amethyst. This can create problems.</string> + <string name="backup_card_concord_emptied">You emptied your Concord list</string> + <string name="backup_card_concord_rewritten">You changed your Concord list</string> + <plurals name="backup_card_dm_relays_lost"> + <item quantity="one">You removed %1$d DM relay</item> + <item quantity="other">You removed %1$d DM relays</item> + </plurals> + <plurals name="backup_card_feeds_removed"> + <item quantity="one">You removed %1$d favorite feed</item> + <item quantity="other">You removed %1$d favorite feeds</item> + </plurals> + <plurals name="backup_card_follows_dropped"> + <item quantity="one">You unfollowed %1$d person</item> + <item quantity="other">You unfollowed %1$d people</item> + </plurals> + <plurals name="backup_card_hashtags_unfollowed"> + <item quantity="one">You unfollowed %1$d hashtag</item> + <item quantity="other">You unfollowed %1$d hashtags</item> + </plurals> + <plurals name="backup_card_indexer_relays_lost"> + <item quantity="one">You removed %1$d indexer relay</item> + <item quantity="other">You removed %1$d indexer relays</item> + </plurals> + <plurals name="backup_card_key_package_relays_lost"> + <item quantity="one">You removed %1$d group-chat key relay</item> + <item quantity="other">You removed %1$d group-chat key relays</item> + </plurals> + <plurals name="backup_card_leave_chats"> + <item quantity="one">You left %1$d public chat</item> + <item quantity="other">You left %1$d public chats</item> + </plurals> + <plurals name="backup_card_leave_communities"> + <item quantity="one">You left %1$d community</item> + <item quantity="other">You left %1$d communities</item> + </plurals> + <plurals name="backup_card_leave_groups"> + <item quantity="one">You left %1$d group</item> + <item quantity="other">You left %1$d groups</item> + </plurals> + <plurals name="backup_card_leave_rooms"> + <item quantity="one">You left %1$d chat room</item> + <item quantity="other">You left %1$d chat rooms</item> + </plurals> + <plurals name="backup_card_mutes_visible"> + <item quantity="one">You unmuted %1$d thing</item> + <item quantity="other">You unmuted %1$d things</item> + </plurals> + <plurals name="backup_card_nip65_lost"> + <item quantity="one">You removed %1$d public relay</item> + <item quantity="other">You removed %1$d public relays</item> + </plurals> + <string name="backup_card_nutzap_key_removed">You removed your nutzap key</string> + <string name="backup_card_nutzap_key_replaced">You replaced your nutzap key</string> + <plurals name="backup_card_nutzap_mints_removed"> + <item quantity="one">You removed %1$d nutzap mint</item> + <item quantity="other">You removed %1$d nutzap mints</item> + </plurals> + <plurals name="backup_card_offers_removed"> + <item quantity="one">You removed %1$d BOLT12 offer</item> + <item quantity="other">You removed %1$d BOLT12 offers</item> + </plurals> + <plurals name="backup_card_payment_targets_removed"> + <item quantity="one">You removed %1$d way to get paid</item> + <item quantity="other">You removed %1$d ways to get paid</item> + </plurals> + <plurals name="backup_card_places_unfollowed"> + <item quantity="one">You unfollowed %1$d place</item> + <item quantity="other">You unfollowed %1$d places</item> + </plurals> + <plurals name="backup_card_private_outbox_lost"> + <item quantity="one">You removed %1$d private relay</item> + <item quantity="other">You removed %1$d private relays</item> + </plurals> + <plurals name="backup_card_profile_lost_fields"> + <item quantity="one">You removed %1$d profile field</item> + <item quantity="other">You removed %1$d profile fields</item> + </plurals> + <plurals name="backup_card_relay_feeds_lost"> + <item quantity="one">You removed %1$d relay feed</item> + <item quantity="other">You removed %1$d relay feeds</item> + </plurals> + <plurals name="backup_card_search_relays_lost"> + <item quantity="one">You removed %1$d search relay</item> + <item quantity="other">You removed %1$d search relays</item> + </plurals> + <plurals name="backup_card_trust_services_changed"> + <item quantity="one">You changed the provider of %1$d trust service</item> + <item quantity="other">You changed the provider of %1$d trust services</item> + </plurals> + <plurals name="backup_card_trusted_lost"> + <item quantity="one">You stopped trusting %1$d relay</item> + <item quantity="other">You stopped trusting %1$d relays</item> + </plurals> + <plurals name="backup_card_unblocked"> + <item quantity="one">You unblocked %1$d relay</item> + <item quantity="other">You unblocked %1$d relays</item> + </plurals> + <string name="backup_card_wallet_emptied">You emptied your Cashu wallet</string> + <string name="backup_card_wallet_rewritten">You changed your Cashu wallet</string> + <string name="backup_conflict_added">Added (%1$s)</string> + <string name="backup_conflict_changed">Changed (%1$s)</string> + <string name="backup_conflict_count_added">%1$s added</string> + <string name="backup_conflict_count_changed">%1$s changed</string> + <string name="backup_conflict_count_removed">%1$s removed</string> + <string name="backup_conflict_keep_new">Use new version</string> + <string name="backup_conflict_private_added">You added private (encrypted) items.</string> + <string name="backup_conflict_private_changed">You also changed private (encrypted) items. They can't be compared here.</string> + <string name="backup_conflict_private_cleared">You removed all private (encrypted) items.</string> + <string name="backup_conflict_relay_read_only">read only</string> + <string name="backup_conflict_relay_read_write">read and write</string> + <string name="backup_conflict_relay_write_only">write only</string> + <string name="backup_conflict_removed">Removed (%1$s)</string> + <string name="backup_conflict_restore_mine">Restore saved version</string> + <string name="backup_conflict_title">You changed your %1$s in another app</string> + <string name="backup_entry_algo_feed">Feeds</string> + <string name="backup_entry_chat_room">Chat rooms</string> + <string name="backup_entry_community">Communities</string> + <string name="backup_entry_hashtag">Hashtags</string> + <string name="backup_entry_identity_claim">Linked identities</string> + <string name="backup_entry_location">Locations</string> + <string name="backup_entry_mint">Mints</string> + <string name="backup_entry_nutzap_key">Nutzap keys</string> + <string name="backup_entry_offer">Offers</string> + <string name="backup_entry_other">Other entries</string> + <string name="backup_entry_payment_target">Payment addresses</string> + <string name="backup_entry_person">People</string> + <string name="backup_entry_profile_field">Profile fields</string> + <string name="backup_entry_public_chat">Public chats</string> + <string name="backup_entry_relay">Relays</string> + <string name="backup_entry_relay_group">Groups</string> + <string name="backup_entry_thread">Threads</string> + <string name="backup_entry_trust_provider">Trust providers</string> + <string name="backup_entry_word">Words</string> + <string name="backup_profile_field_about">About</string> + <string name="backup_profile_field_banner">Banner</string> + <string name="backup_profile_field_birthday">Birthday</string> + <string name="backup_profile_field_bot">Bot flag</string> + <string name="backup_profile_field_clink_offer">CLINK offer</string> + <string name="backup_profile_field_display_name">Display name</string> + <string name="backup_profile_field_lud06">LNURL</string> + <string name="backup_profile_field_lud16">Lightning address</string> + <string name="backup_profile_field_name">Username</string> + <string name="backup_profile_field_nip05">Nostr address</string> + <string name="backup_profile_field_picture">Picture</string> + <string name="backup_profile_field_pronouns">Pronouns</string> + <string name="backup_profile_field_website">Website</string> + <string name="backup_review_allowed_tag">Allowed</string> + <string name="backup_review_changed_by_other_app">Changed in another app · %1$s</string> + <string name="backup_review_concord_body">Your Concord communities are encrypted to you, so they can't be compared here. Restoring puts back exactly what this device saved.</string> + <string name="backup_review_demoted_tag">Moved</string> + <string name="backup_review_dropped_count">%1$s dropped</string> + <string name="backup_review_dropped_tag">Dropped</string> + <string name="backup_review_encrypted_added">You created it</string> + <string name="backup_review_encrypted_changed">You changed it</string> + <string name="backup_review_encrypted_cleared">You emptied it</string> + <string name="backup_review_feed">Feed</string> + <string name="backup_review_gone">Gone</string> + <string name="backup_review_hashtags_you_follow">Hashtags you follow</string> + <string name="backup_review_inbox">Inbox</string> + <string name="backup_review_inbox_explainer">where others reach you</string> + <string name="backup_review_joined">joined</string> + <string name="backup_review_kept">kept</string> + <string name="backup_review_kept_count">%1$s kept</string> + <string name="backup_review_key_removed">Receiving key removed</string> + <string name="backup_review_key_replaced">Receiving key replaced</string> + <string name="backup_review_key_warning">Nutzaps will be locked to this new key. If this device doesn't have it, you may not be able to redeem them here.</string> + <string name="backup_review_left">left</string> + <string name="backup_review_left_tag">Left</string> + <string name="backup_review_listed_tag">Listed</string> + <string name="backup_review_moved_tag">Moved</string> + <string name="backup_review_names_and_more">%1$s and %2$s more</string> + <string name="backup_review_new">New version</string> + <string name="backup_review_new_count">%1$s new</string> + <string name="backup_review_new_count_caption">new</string> + <string name="backup_review_new_tag">New</string> + <string name="backup_review_newly_blocked">Newly blocked</string> + <string name="backup_review_newly_muted">%1$s newly muted</string> + <string name="backup_review_no_longer_blocked">No longer blocked</string> + <string name="backup_review_not_set">Not set</string> + <string name="backup_review_nothing_here">Nothing here</string> + <string name="backup_review_offer">BOLT12 offer</string> + <plurals name="backup_review_offers_gone"> + <item quantity="one">%1$d offer removed</item> + <item quantity="other">%1$d offers removed</item> + </plurals> + <string name="backup_review_outbox">Outbox</string> + <string name="backup_review_outbox_explainer">where you publish</string> + <plurals name="backup_review_payment_targets_gone"> + <item quantity="one">%1$d way to pay you removed</item> + <item quantity="other">%1$d ways to pay you removed</item> + </plurals> + <string name="backup_review_payments_explainer">People paying you only see what's in this list.</string> + <string name="backup_review_places_gone">places gone</string> + <string name="backup_review_places_you_follow">Places you follow</string> + <string name="backup_review_private_added_body">They're encrypted now, which is likely where the public items above went.</string> + <string name="backup_review_private_added_title">You made items private</string> + <string name="backup_review_private_changed_body">They're encrypted, so they can't be compared here.</string> + <string name="backup_review_private_changed_title">You changed private items</string> + <string name="backup_review_private_wiped_body">They're encrypted, so only you can see what they were. Restoring brings them all back.</string> + <string name="backup_review_private_wiped_title">You removed all private items</string> + <string name="backup_review_receiving_tag">Receiving</string> + <string name="backup_review_renamed">renamed</string> + <string name="backup_review_renamed_tag">Renamed</string> + <string name="backup_review_saved">On this device</string> + <string name="backup_review_search_people">Search %1$s people</string> + <string name="backup_review_searching_tag">Searching</string> + <string name="backup_review_service">Service</string> + <string name="backup_review_service_followers">Followers</string> + <string name="backup_review_service_rank">Rank</string> + <string name="backup_review_service_topics">Topics</string> + <string name="backup_review_services_changed">%1$s of %2$s services changed</string> + <string name="backup_review_services_changed_explainer">They now come from someone else, or no one</string> + <string name="backup_review_still_blocked">Still blocked</string> + <string name="backup_review_storing_tag">Storing</string> + <string name="backup_review_tab_dropped">Dropped · %1$s</string> + <string name="backup_review_tab_edited">Edited · %1$s</string> + <string name="backup_review_tab_new">New · %1$s</string> + <string name="backup_review_title_cleared">%1$s cleared</string> + <string name="backup_review_title_updated">%1$s updated</string> + <string name="backup_review_topics_gone">topics gone</string> + <string name="backup_review_trust_warning">Ranks, follower counts and spam filtering you see come from these providers.</string> + <string name="backup_review_trusted_tag">Trusted</string> + <string name="backup_review_unblocked">%1$s unblocked</string> + <string name="backup_review_unblocked_explainer">Amethyst will connect to them again</string> + <string name="backup_review_units_tag">Units</string> + <string name="backup_review_unmuted">%1$s unmuted</string> + <string name="backup_review_unmuted_explainer">These can reach your feeds again</string> + <string name="backup_review_untrusted_tag">Untrusted</string> + <string name="backup_review_wallet_body">Your wallet's mints and keys are encrypted to you, so they can't be compared item by item here. Restoring puts back exactly what this device saved.</string> + <string name="backup_review_was_named">was “%1$s”</string> + <string name="backup_review_words_and_hashtags">Words & hashtags</string> + <string name="backup_type_app_settings">synced app settings</string> + <string name="backup_type_app_settings_explainer">Amethyst settings synced across your devices. They are encrypted, so only their presence can be compared.</string> + <string name="backup_type_blocked_relays">blocked relay list</string> + <string name="backup_type_blocked_relays_explainer">Relays this app never connects to.</string> + <string name="backup_type_bolt12_offers">BOLT12 offers</string> + <string name="backup_type_bolt12_offers_explainer">The lightning offers others use to pay you.</string> + <string name="backup_type_cashu_wallet">Cashu wallet</string> + <string name="backup_type_cashu_wallet_explainer">Your ecash wallet settings and mints. They are encrypted, so only their presence can be compared.</string> + <string name="backup_type_communities">community list</string> + <string name="backup_type_communities_explainer">The communities you joined.</string> + <string name="backup_type_concord_communities">Concord community list</string> + <string name="backup_type_concord_communities_explainer">The Concord communities you joined. They are encrypted, so only their presence can be compared.</string> + <string name="backup_type_dm_relays">DM relay list</string> + <string name="backup_type_dm_relays_explainer">Where others deliver your private messages.</string> + <string name="backup_type_ephemeral_chats">ephemeral chat list</string> + <string name="backup_type_ephemeral_chats_explainer">The ephemeral chat rooms you joined.</string> + <string name="backup_type_favorite_algo_feeds">favorite feeds list</string> + <string name="backup_type_favorite_algo_feeds_explainer">The algorithmic feeds you saved.</string> + <string name="backup_type_follow_list">follow list</string> + <string name="backup_type_follow_list_explainer">Decides whose posts appear in your home feed.</string> + <string name="backup_type_geohashes">followed locations</string> + <string name="backup_type_geohashes_explainer">Locations whose posts appear in your feeds.</string> + <string name="backup_type_hashtags">followed hashtags</string> + <string name="backup_type_hashtags_explainer">Hashtags whose posts appear in your feeds.</string> + <string name="backup_type_indexer_relays">indexer relay list</string> + <string name="backup_type_indexer_relays_explainer">The relays used to find other people's profiles and relay lists.</string> + <string name="backup_type_key_package_relays">group-chat key relays</string> + <string name="backup_type_key_package_relays_explainer">Where others find the keys to add you to encrypted group chats.</string> + <string name="backup_type_mute_list">mute list</string> + <string name="backup_type_mute_list_explainer">Hides people, words, hashtags and threads from everything you see.</string> + <string name="backup_type_nutzap_info">nutzap receiving settings</string> + <string name="backup_type_nutzap_info_explainer">The mints and key others use to send you nutzaps.</string> + <string name="backup_type_other">list</string> + <string name="backup_type_other_explainer">A list saved on this device.</string> + <string name="backup_type_outbox_inbox_relays">public relay list</string> + <string name="backup_type_outbox_inbox_relays_explainer">Tells everyone where to find your posts and where to send you replies and mentions.</string> + <string name="backup_type_payment_targets">payment addresses</string> + <string name="backup_type_payment_targets_explainer">The ways other people can pay you.</string> + <string name="backup_type_private_outbox_relays">private relay list</string> + <string name="backup_type_private_outbox_relays_explainer">Where your drafts and private data are stored.</string> + <string name="backup_type_profile">profile</string> + <string name="backup_type_profile_explainer">Your name, picture, bio and payment addresses, as everyone sees them.</string> + <string name="backup_type_public_chats">public chat list</string> + <string name="backup_type_public_chats_explainer">The public chats you joined.</string> + <string name="backup_type_relay_feeds">favorite relay feeds</string> + <string name="backup_type_relay_feeds_explainer">The relays you browse as feeds.</string> + <string name="backup_type_relay_groups">group list</string> + <string name="backup_type_relay_groups_explainer">The relay-based groups you joined.</string> + <string name="backup_type_search_relays">search relay list</string> + <string name="backup_type_search_relays_explainer">The relays used when you search.</string> + <string name="backup_type_trust_providers">trust provider list</string> + <string name="backup_type_trust_providers_explainer">The services you trust to rank and verify people for you.</string> + <string name="backup_type_trusted_relays">trusted relay list</string> + <string name="backup_type_trusted_relays_explainer">Relays you trust to connect to without asking.</string> + <string name="cordn_action_delete">Delete</string> + <string name="cordn_action_edit">Edit</string> + <string name="cordn_action_editing">Editing your message</string> + <string name="cordn_action_go_to_message">Go to message</string> + <string name="cordn_action_pin">Pin</string> + <string name="cordn_action_reply">Reply</string> + <string name="cordn_action_unpin">Unpin</string> + <string name="cordn_admin_action_failed">That change could not be made.</string> + <string name="cordn_admin_coordinator_silent">The coordinator did not answer, so nothing was changed. Try again in a moment.</string> + <string name="cordn_admin_no_key_package">%1$s has not published a key package to this coordinator, so they cannot be added yet. Ask them to open cordn on this coordinator first.</string> + <string name="cordn_admin_no_self_remove">You cannot remove yourself from a cordn group.</string> + <string name="cordn_admin_no_welcome">%1$s could not be given a way into the group, so the add was abandoned.</string> + <string name="cordn_admin_not_a_member">%1$s is not in this group.</string> + <string name="cordn_admin_someone">That person</string> + <string name="cordn_admin_wrong_key_package">This coordinator served a key package that belongs to someone else, so %1$s was not added. Nothing was changed.</string> + <string name="cordn_backup_contents_body">Your group encryption keys, your key packages, and every message in those groups. Anyone who opens it can read the conversations. The passphrase is the only thing protecting it once it leaves Amethyst.</string> + <string name="cordn_backup_contents_title">What is in the file</string> + <string name="cordn_backup_explainer">Your nsec cannot bring cordn groups back. A cordn group lives as encryption state on this device and an ordered stream on a coordinator that cannot read it — lose the device without a backup and the group is gone, including everything already said in it.</string> + <string name="cordn_backup_export">Export</string> + <string name="cordn_backup_exported">Backup saved.</string> + <string name="cordn_backup_failed">That did not work. Check the passphrase and the file.</string> + <string name="cordn_backup_passphrase">Passphrase</string> + <string name="cordn_backup_restore">Restore</string> + <string name="cordn_backup_restore_body">Restoring replaces the cordn groups on this device with the ones in the file. It is for a phone that has taken over from another — not a way to use the same groups on two devices at once.</string> + <string name="cordn_backup_restore_confirm_body">Every cordn group now on this device will be removed and replaced by the ones in the file.\n\nDo not restore onto a second phone while the first is still in use. Both would hold the same group keys, and once both have sent a message the group breaks for everyone — silently, and with no way back.</string> + <string name="cordn_backup_restore_confirm_title">Replace this device's cordn groups?</string> + <string name="cordn_backup_restore_title">Restore</string> + <string name="cordn_backup_restored">Restored.</string> + <string name="cordn_chat_empty_description">Anything sent here is encrypted for this group. The coordinator relays it without being able to read it.</string> + <string name="cordn_chat_empty_title">No messages yet</string> + <string name="cordn_chat_unread_divider">New messages</string> + <string name="cordn_chat_yesterday">Yesterday</string> + <string name="cordn_composer_hint">Message</string> + <string name="cordn_coordinators_add_action">Add</string> + <string name="cordn_coordinators_add_failed">Could not open that coordinator.</string> + <string name="cordn_coordinators_copy_key">Copy the coordinator's npub</string> + <string name="cordn_coordinators_discover_action">Look for coordinators</string> + <string name="cordn_coordinators_discover_add">Add</string> + <string name="cordn_coordinators_discover_explainer">Reads announcements your relays already carry. No coordinator is contacted, so none of them learns you looked.</string> + <string name="cordn_coordinators_discover_failed">Could not read announcements</string> + <string name="cordn_coordinators_discover_none">Nobody is announcing on your relays.</string> + <string name="cordn_coordinators_discover_seen">Announced %1$s ago</string> + <string name="cordn_coordinators_discover_seen_on">Last announced %1$s</string> + <string name="cordn_coordinators_discover_unheard">Nothing found, and %1$d of your relays did not answer.</string> + <string name="cordn_coordinators_explainer">A coordinator orders one group's messages and is the only thing that can. Losing it loses the ordering, and a second one does not mirror the first — so these are named things you choose, not interchangeable relays.</string> + <string name="cordn_coordinators_health_down">Not answering — %1$d calls in a row have failed. Groups on it are not syncing.</string> + <string name="cordn_coordinators_health_ok">Answering.</string> + <string name="cordn_coordinators_health_retrying">A call failed; retrying.</string> + <string name="cordn_coordinators_health_unknown">Nothing asked of it yet.</string> + <string name="cordn_coordinators_hide_older">Hide the ones that stopped announcing</string> + <string name="cordn_coordinators_identify">Ask who it is</string> + <string name="cordn_coordinators_key">Coordinator key</string> + <string name="cordn_coordinators_label">A name for it (yours, optional)</string> + <string name="cordn_coordinators_label_note">This name is yours and stays on this device. A coordinator cannot prove a name, so it is never asked for one.</string> + <string name="cordn_coordinators_more">More actions</string> + <string name="cordn_coordinators_none">No coordinators yet.</string> + <string name="cordn_coordinators_purge">Purge</string> + <string name="cordn_coordinators_purge_body">Removing keeps the groups on this device, so adding the coordinator back brings them with it. Purging deletes them: the group keys, the read positions and your key packages all go, and those groups can only be re-entered by a fresh invitation.\n\nThe coordinator is not told and keeps whatever it already had.</string> + <string name="cordn_coordinators_purge_title">Purge this coordinator?</string> + <string name="cordn_coordinators_relays">%1$s</string> + <string name="cordn_coordinators_relays_label">Answers on</string> + <string name="cordn_coordinators_relays_more">%1$s +%2$d more</string> + <string name="cordn_coordinators_remove">Remove</string> + <string name="cordn_coordinators_rename">Rename</string> + <string name="cordn_coordinators_server">Says it is: %1$s</string> + <string name="cordn_coordinators_server_claim">A coordinator's name and version are strings it chose. Its public key is the only thing that identifies it.</string> + <string name="cordn_coordinators_server_silent">Answered, but named nothing.</string> + <string name="cordn_coordinators_show_all">Show all %1$d</string> + <string name="cordn_coordinators_show_fewer">Show fewer</string> + <string name="cordn_coordinators_show_older">Show %1$d that stopped announcing</string> + <string name="cordn_coordinators_stale_note">These have not announced in over a month. Creating a group on one that has gone away will fail.</string> + <string name="cordn_create_action">Create group</string> + <plurals name="cordn_create_action_invite"> + <item quantity="one">Create and invite %1$d person</item> + <item quantity="other">Create and invite %1$d people</item> + </plurals> + <string name="cordn_create_add_member">Add someone</string> + <string name="cordn_create_admin">Admin</string> + <plurals name="cordn_create_admin_count"> + <item quantity="one">%1$d person can add and remove</item> + <item quantity="other">%1$d people can add and remove</item> + </plurals> + <string name="cordn_create_admin_only_me">Only I manage this group</string> + <string name="cordn_create_admin_only_me_on">Only you can invite, remove members or change the group's details. You can hand that to someone else later.</string> + <string name="cordn_create_coordinator">Coordinator</string> + <string name="cordn_create_coordinator_change">Change</string> + <string name="cordn_create_coordinator_discover">Find coordinators</string> + <string name="cordn_create_coordinator_hide">Done</string> + <string name="cordn_create_coordinator_new">Another coordinator</string> + <string name="cordn_create_coordinator_pubkey">Coordinator public key (hex or npub)</string> + <string name="cordn_create_coordinator_relays">Relays it answers on, one per line</string> + <string name="cordn_create_coverage_all">Reaches all %1$d of your people</string> + <string name="cordn_create_coverage_partial">Reaches %1$d of %2$d — the rest would be left out</string> + <string name="cordn_create_coverage_unknown">Not checked — picking it will ask</string> + <string name="cordn_create_description">Description (optional)</string> + <string name="cordn_create_egalitarian">Everyone can add and remove</string> + <string name="cordn_create_egalitarian_note">Everyone in the group can invite, remove members and change its details. Any member can name admins later, which ends that.</string> + <string name="cordn_create_explainer">A cordn group is ordered by one coordinator you choose. Creating the group happens on this device — the coordinator only learns the group exists once you invite someone or send a message.</string> + <string name="cordn_create_failed">The group could not be created.</string> + <string name="cordn_create_name">Group name</string> + <string name="cordn_create_no_coordinator">No coordinator chosen</string> + <string name="cordn_create_outcome_explainer">Each invitation is a Welcome left on the coordinator, so they can fail one at a time. Nobody has been notified: they see it next time they open their app.</string> + <string name="cordn_create_outcome_failed">The coordinator refused the invitation</string> + <string name="cordn_create_outcome_link">Send a link</string> + <string name="cordn_create_outcome_no_welcome">Added to the group, but the invitation did not reach them — try again from the group screen</string> + <string name="cordn_create_outcome_open">Open the chat</string> + <string name="cordn_create_outcome_skipped">No key here, so no Welcome could be left</string> + <string name="cordn_create_outcome_title">The group exists</string> + <string name="cordn_create_outcome_waiting">Waiting for them</string> + <string name="cordn_create_people_done">Done</string> + <string name="cordn_create_people_empty">Nobody added yet. A group can be created empty and invitations sent later, but naming people now is what shows which coordinator can reach them.</string> + <string name="cordn_create_people_none">Nobody yet — tap to add people</string> + <string name="cordn_create_people_note">Naming people now is what tells us which coordinator can reach them. Each one gets a Welcome left on that coordinator, which they pick up next time they open their app.</string> + <plurals name="cordn_create_reach_count"> + <item quantity="one">Reachable on %1$d coordinator</item> + <item quantity="other">Reachable on %1$d coordinators</item> + </plurals> + <string name="cordn_create_reach_none">No key on any coordinator you use</string> + <string name="cordn_create_reach_unknown">Not checked yet</string> + <string name="cordn_create_step_admins">Who can add and remove</string> + <string name="cordn_create_step_name">Name it</string> + <string name="cordn_create_step_people">Who is in it</string> + <string name="cordn_create_step_where">Where it lives</string> + <string name="cordn_create_title">New Cordn group</string> + <plurals name="cordn_create_unreachable_count"> + <item quantity="one">%1$d person cannot be reached by any coordinator you use</item> + <item quantity="other">%1$d people cannot be reached by any coordinator you use</item> + </plurals> + <string name="cordn_create_you">You, always an admin</string> + <string name="cordn_delete_confirm_body">Everyone in the group will see this message as withdrawn, and Amethyst will stop showing its text. The coordinator still holds the encrypted copy it was sent as, and anyone who already read it has already read it.</string> + <string name="cordn_delete_confirm_title">Withdraw message</string> + <string name="cordn_delivery_accepted">Accepted by the coordinator</string> + <string name="cordn_exposure_close">Got it</string> + <string name="cordn_exposure_open">What this coordinator can see</string> + <string name="cordn_group_info">Group info</string> + <string name="cordn_group_unavailable">This group is not available. Its coordinator may have been removed.</string> + <string name="cordn_group_unavailable_detail">Add a coordinator in Settings to start using cordn groups.</string> + <plurals name="cordn_groups_count"> + <item quantity="one">%1$d group</item> + <item quantity="other">%1$d groups</item> + </plurals> + <string name="cordn_groups_down_note">Nothing is arriving from these groups. Amethyst keeps retrying.</string> + <string name="cordn_groups_manage">Coordinators</string> + <string name="cordn_groups_none">No cordn groups yet</string> + <string name="cordn_groups_none_detail">A cordn group lives on one coordinator that holds its membership and passes messages along, without ever seeing what anyone writes.</string> + <string name="cordn_groups_start">Start one</string> + <string name="cordn_info_add_member">Add someone</string> + <string name="cordn_info_add_member_none">Nobody found by that name.</string> + <string name="cordn_info_add_member_note">They can only be added if they published a key package to this coordinator.</string> + <string name="cordn_info_add_member_placeholder">Name, npub or name@domain</string> + <string name="cordn_info_admin_badge">Admin</string> + <plurals name="cordn_info_admins_kept"> + <item quantity="one">The group's %1$d admin stays as it is.</item> + <item quantity="other">The group's %1$d admins stay as they are.</item> + </plurals> + <string name="cordn_info_coordinator">Coordinator</string> + <string name="cordn_info_coordinator_key">Coordinator key</string> + <string name="cordn_info_coordinator_nprofile">Coordinator link</string> + <string name="cordn_info_copy_nprofile">Copy the coordinator's nprofile</string> + <string name="cordn_info_edit_details">Edit details</string> + <string name="cordn_info_egalitarian">This group has no admins: everyone can add, remove and rename, permanently.</string> + <string name="cordn_info_epoch">Epoch</string> + <string name="cordn_info_gid">Group id</string> + <string name="cordn_info_has_key_package">Can be added on this coordinator</string> + <string name="cordn_info_members">Members</string> + <string name="cordn_info_remove_confirm_body">%1$s stops receiving messages from here. They keep what they have already read, and this cannot be undone — they would have to be invited again.</string> + <string name="cordn_info_remove_confirm_title">Remove from group?</string> + <string name="cordn_info_remove_member">Remove</string> + <string name="cordn_info_save">Save</string> + <string name="cordn_info_technical">Technical details</string> + <string name="cordn_invitations_accept">Join</string> + <string name="cordn_invitations_decline">Decline</string> + <string name="cordn_invitations_decline_warning">Declining is permanent. Getting back in means being invited again.</string> + <string name="cordn_invitations_explainer">Checked when you open this screen, and at no other time — asking a coordinator whether anyone invited you tells it you are here, so Amethyst does not do it in the background.</string> + <string name="cordn_invitations_load_failed">Could not read invitations.</string> + <plurals name="cordn_invitations_members"> + <item quantity="one">%1$d person is already in this group</item> + <item quantity="other">%1$d people are already in this group</item> + </plurals> + <string name="cordn_invitations_members_more">+%1$d more</string> + <string name="cordn_invitations_no_coordinators">You have no coordinator open. cordn invitations arrive through a coordinator, so there is nowhere to look yet.</string> + <string name="cordn_invitations_none">No invitations waiting.</string> + <string name="cordn_invitations_refresh">Check again</string> + <string name="cordn_invitations_skipped">An invitation was left for another device</string> + <string name="cordn_invitations_title">Cordn invitations</string> + <string name="cordn_invitations_unreachable">%1$s did not answer</string> + <string name="cordn_invitations_via">Through %1$s</string> + <string name="cordn_keypackages_disclosure">Publishing signs a record under your own account key. That coordinator then knows this account exists and is invitable — permanently, and whether or not anyone invites you.</string> + <string name="cordn_keypackages_explainer">Others can only add you to a cordn group by taking a key package you published to that coordinator. cordn has no other place to keep one, so with none published nobody can invite you — and nothing tells them why.</string> + <string name="cordn_keypackages_failed">Could not read the key packages.</string> + <string name="cordn_keypackages_last_resort_no">No last-resort package. Once the single-use ones run out, invitations fail.</string> + <string name="cordn_keypackages_last_resort_yes">A last-resort package is published, so an invitation can still be made once the single-use ones run out.</string> + <plurals name="cordn_keypackages_orphans"> + <item quantity="one">%1$d package this device cannot open</item> + <item quantity="other">%1$d packages this device cannot open</item> + </plurals> + <string name="cordn_keypackages_orphans_body">The coordinator will hand these to anyone inviting you, but the private half is not on this device — it belongs to another install. If that install is gone, an invitation using one of these produces a welcome nobody can ever open.</string> + <string name="cordn_keypackages_publish">Publish one</string> + <string name="cordn_keypackages_publish_last_resort">Publish last-resort</string> + <plurals name="cordn_keypackages_summary"> + <item quantity="one">%1$d single-use package available.</item> + <item quantity="other">%1$d single-use packages available.</item> + </plurals> + <string name="cordn_keypackages_topup_note">Once you have published here, Amethyst quietly replaces packages as they are used up. It never publishes the first one for you.</string> + <string name="cordn_keypackages_withdraw_all">Withdraw all</string> + <string name="cordn_keypackages_withdraw_orphans">Withdraw those</string> + <string name="cordn_link_request">Ask to join this group</string> + <string name="cordn_link_request_disclosure">This publishes a key package under your own account key and tells the coordinator you want into this group — whether or not anyone answers.</string> + <string name="cordn_link_request_failed">Could not ask to join.</string> + <string name="cordn_link_request_sent">Asked. A member of the group has to add you; the invitation will show up under cordn invitations.</string> + <string name="cordn_link_scan">Scan</string> + <string name="cordn_media_download_failed">Could not open that file.</string> + <string name="cordn_media_no_server">No media server is set for this account, so there is nowhere to put the file. Pick one in Settings.</string> + <string name="cordn_media_unreadable">That file could not be read.</string> + <string name="cordn_media_upload_failed">Could not send that file.</string> + <plurals name="cordn_member_count"> + <item quantity="one">%1$d member</item> + <item quantity="other">%1$d members</item> + </plurals> + <string name="cordn_message_deleted">Message deleted</string> + <string name="cordn_message_details_coordinator">Coordinator</string> + <string name="cordn_message_details_cursor">Cursor</string> + <string name="cordn_message_details_sent_at">Sent</string> + <string name="cordn_message_details_title">Message details</string> + <string name="cordn_message_edited">edited</string> + <string name="cordn_pinned_by">Pinned by %1$s</string> + <plurals name="cordn_pinned_count"> + <item quantity="one">%1$d pinned message in this group</item> + <item quantity="other">%1$d pinned messages in this group</item> + </plurals> + <string name="cordn_pinned_next">Next pinned message</string> + <string name="cordn_pinned_previous">Previous pinned message</string> + <string name="cordn_pinned_show_all">Show all pinned messages</string> + <string name="cordn_pinned_title">Pinned messages</string> + <string name="cordn_reactions_title">Reactions</string> + <string name="cordn_requests_accept">Add</string> + <string name="cordn_requests_admin_only">Only an admin can accept. In a group with no admins, that is everyone.</string> + <string name="cordn_requests_check">Check for requests</string> + <string name="cordn_requests_decline">Dismiss</string> + <string name="cordn_requests_failed">Could not read the requests.</string> + <string name="cordn_requests_none">Nobody is waiting to join.</string> + <string name="cordn_requests_title">Requests to join</string> + <string name="cordn_send">Send</string> + <string name="cordn_send_failed">That did not send.</string> + <string name="cordn_send_no_session">This group's coordinator is not open, so nothing can be sent yet.</string> + <string name="cordn_share_copied">Copied</string> + <string name="cordn_share_copy">Copy link</string> + <string name="cordn_share_explainer">Anyone with this link can ask to join. An admin still has to let them in.</string> + <string name="cordn_share_title">Share this group</string> + <string name="cordn_voice_play">Play voice note</string> + <string name="cordn_voice_record">Record a voice note</string> + <string name="cordn_voice_stop">Stop and send</string> </resources> From 9b26de4515fab797300d7ee84d26a09613cbf992 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 21:13:14 +0000 Subject: [PATCH 16/60] refactor(commonsUI): move the remaining movable layouts, feed helpers and link components Most of the sweep's "Batch 4" had already landed in other PRs; this moves what was left that had no real blocker. - NewItemsBubble -> commonsUI components. - ChatHeaderLayout, LeftPictureLayout, listItem/SlimListItemLayout -> commonsUI layouts. Their @Previews use app drawables, TimeAgo and TextCount, so the previews stay in the app as *Preview.kt files. - WatchScrollToTop (list/grid/pager) -> commonsUI feeds; the CardFeedContentState overload stays in the app. The app's StickToTopOnPrepend was a copy of the commons one and is deleted; the commons one now collects with collectAsStateWithLifecycle, as the app copy did. - ChannelFeedState, ChannelFeedContentState -> commons feeds (headless). checkNotInMainThread() becomes LocalCache.appHost.assertNotMainThread(), which Android wires to the same check. - ClickableBuzzInviteLink, ClickableConcordInviteLink, ClickableRelayGroupLink, ClickableRelayUrl, OutlinedThinPaddingTextField -> commonsUI components. - The app's ClickableEmail/ClickablePhone are deleted in favour of the commons ones, which open mailto:/tel: through LocalUriHandler rather than ACTION_SEND/ACTION_DIAL intents. The commons ClickablePhone is new. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjQQN9CgWHVtNCSWqnKoqY --- .../amethyst/ui/actions/EditPostView.kt | 2 +- .../ui/components/AmethystRichText.kt | 6 + .../amethyst/ui/components/ClickableEmail.kt | 60 ------ .../ui/components/ConcordInviteCard.kt | 1 + .../amethyst/ui/components/RelayGroupCard.kt | 1 + .../amethyst/ui/components/RichTextViewer.kt | 6 + .../amethyst/ui/feeds/FeedContentStateView.kt | 2 + .../amethyst/ui/feeds/WatchScrollToTop.kt | 204 ------------------ ...erLayout.kt => ChatHeaderLayoutPreview.kt} | 53 +---- ...eLayout.kt => LeftPictureLayoutPreview.kt} | 53 +---- .../layouts/listItem/SlimListItemPreview.kt | 132 ++++++++++++ .../amethyst/ui/note/UserCompose.kt | 2 +- .../ui/note/creators/notify/AudienceSheet.kt | 2 +- .../userSuggestions/ShowUserSuggestionList.kt | 2 +- .../note/creators/zapsplits/ForwardZapTo.kt | 2 +- .../amethyst/ui/screen/FeedView.kt | 4 +- .../amethyst/ui/screen/UserFeedView.kt | 2 +- .../loggedIn/AccountFeedContentStates.kt | 2 +- .../calendars/CalendarCollectionsView.kt | 2 +- .../loggedIn/calendars/CalendarFeedView.kt | 2 +- .../chats/rooms/ChatroomHeaderCompose.kt | 9 +- .../RenderPublicChatChannelThumb.kt | 2 +- .../nip72Communities/CommunityCard.kt | 2 +- .../loggedIn/discover/nip90DVMs/DVMCard.kt | 2 +- .../browse/BrowseEmojiSetsScreen.kt | 4 +- .../ui/screen/loggedIn/home/HomeScreen.kt | 4 +- .../loggedIn/notifications/CardFeedView.kt | 2 +- .../notifications/NotificationScreen.kt | 1 + .../profile/newthreads/TabNotesNewThreads.kt | 4 +- .../loggedIn/profile/zaps/ZapNoteCompose.kt | 2 +- .../relays/nip86/RelayManagementScreen.kt | 2 +- .../commons}/feeds/ChannelFeedContentState.kt | 14 +- .../commons}/feeds/ChannelFeedState.kt | 3 +- .../ui/components/ClickableBuzzInviteLink.kt | 2 +- .../components/ClickableConcordInviteLink.kt | 2 +- .../commons/ui/components/ClickableLinks.kt | 24 +++ .../ui/components/ClickableRelayGroupLink.kt | 2 +- .../ui/components/ClickableRelayUrl.kt | 2 +- .../commons/ui/components/NewItemsBubble.kt | 29 +-- .../OutlinedThinPaddingTextField.kt | 2 +- .../commons/ui/feeds/StickToTopOnPrepend.kt | 17 +- .../commons/ui/feeds/WatchScrollToTop.kt | 75 +++++++ .../commons/ui/layouts/ChatHeaderLayout.kt | 78 +++++++ .../commons/ui/layouts/LeftPictureLayout.kt | 77 +++++++ .../ui/layouts/listItem/SlimListItemLayout.kt | 126 +---------- 45 files changed, 471 insertions(+), 556 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableEmail.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/{ChatHeaderLayout.kt => ChatHeaderLayoutPreview.kt} (77%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/{LeftPictureLayout.kt => LeftPictureLayoutPreview.kt} (72%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/listItem/SlimListItemPreview.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/feeds/ChannelFeedContentState.kt (92%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/feeds/ChannelFeedState.kt (93%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/components/ClickableBuzzInviteLink.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/components/ClickableConcordInviteLink.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/components/ClickableRelayGroupLink.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/components/ClickableRelayUrl.kt (97%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickablePhone.kt => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/NewItemsBubble.kt (63%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/components/OutlinedThinPaddingTextField.kt (99%) create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/WatchScrollToTop.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/ChatHeaderLayout.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/LeftPictureLayout.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/layouts/listItem/SlimListItemLayout.kt (81%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index 5db9dcbbd4..f7f574dbd6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -78,6 +78,7 @@ import com.vitorpamplona.amethyst.commons.resources.lightning_create_and_add_inv import com.vitorpamplona.amethyst.commons.resources.lightning_invoice import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.commons.ui.components.OutlinedThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar @@ -98,7 +99,6 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.components.BechLink import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview -import com.vitorpamplona.amethyst.ui.components.OutlinedThinPaddingTextField import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AmethystRichText.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AmethystRichText.kt index 2f8261e466..176ba467b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AmethystRichText.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AmethystRichText.kt @@ -52,6 +52,12 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextViewerState import com.vitorpamplona.amethyst.commons.richtext.SecretEmoji import com.vitorpamplona.amethyst.commons.richtext.Segment import com.vitorpamplona.amethyst.commons.richtext.WithdrawSegment +import com.vitorpamplona.amethyst.commons.ui.components.ClickableBuzzInviteLink +import com.vitorpamplona.amethyst.commons.ui.components.ClickableConcordInviteLink +import com.vitorpamplona.amethyst.commons.ui.components.ClickableEmail +import com.vitorpamplona.amethyst.commons.ui.components.ClickablePhone +import com.vitorpamplona.amethyst.commons.ui.components.ClickableRelayGroupLink +import com.vitorpamplona.amethyst.commons.ui.components.ClickableRelayUrl import com.vitorpamplona.amethyst.commons.ui.components.NowhereLinkCard import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.richtext.LocalRichTextInteractions diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableEmail.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableEmail.kt deleted file mode 100644 index 6e25f4be6b..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableEmail.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.components - -import android.content.ActivityNotFoundException -import android.content.Context -import android.content.Intent -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextPrimary -import kotlinx.coroutines.CancellationException - -@Composable -fun ClickableEmail(email: String) { - val stripped = email.replaceFirst("mailto:", "") - val context = LocalContext.current - - ClickableTextPrimary( - text = stripped, - onClick = { runCatching { context.sendMail(stripped) } }, - ) -} - -fun Context.sendMail( - to: String, - subject: String? = null, -) { - try { - val intent = Intent(Intent.ACTION_SEND) - intent.type = "vnd.android.cursor.item/email" // or "message/rfc822" - intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(to)) - if (subject != null) { - intent.putExtra(Intent.EXTRA_SUBJECT, subject) - } - startActivity(intent) - } catch (e: ActivityNotFoundException) { - // TODO: Handle case where no email app is available - } catch (t: Throwable) { - if (t is CancellationException) throw t - // TODO: Handle potential other type of exceptions - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ConcordInviteCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ConcordInviteCard.kt index 8935486076..714fc0af02 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ConcordInviteCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ConcordInviteCard.kt @@ -49,6 +49,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.concord_home_title import com.vitorpamplona.amethyst.commons.resources.concord_invite_card_join import com.vitorpamplona.amethyst.commons.resources.concord_invite_card_subtitle +import com.vitorpamplona.amethyst.commons.ui.components.ClickableConcordInviteLink import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RelayGroupCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RelayGroupCard.kt index cda380757e..e2a636f8c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RelayGroupCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RelayGroupCard.kt @@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.resources.relay_group_badge_invite_onl import com.vitorpamplona.amethyst.commons.resources.relay_group_badge_private import com.vitorpamplona.amethyst.commons.resources.relay_group_member_count import com.vitorpamplona.amethyst.commons.resources.relay_group_open +import com.vitorpamplona.amethyst.commons.ui.components.ClickableRelayGroupLink import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.pluralStringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index ede5174be2..bfa2899272 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -102,6 +102,12 @@ import com.vitorpamplona.amethyst.commons.richtext.Segment import com.vitorpamplona.amethyst.commons.richtext.VideoSegment import com.vitorpamplona.amethyst.commons.richtext.WithdrawSegment import com.vitorpamplona.amethyst.commons.ui.components.AnimatedBorderTextCornerRadius +import com.vitorpamplona.amethyst.commons.ui.components.ClickableBuzzInviteLink +import com.vitorpamplona.amethyst.commons.ui.components.ClickableConcordInviteLink +import com.vitorpamplona.amethyst.commons.ui.components.ClickableEmail +import com.vitorpamplona.amethyst.commons.ui.components.ClickablePhone +import com.vitorpamplona.amethyst.commons.ui.components.ClickableRelayGroupLink +import com.vitorpamplona.amethyst.commons.ui.components.ClickableRelayUrl import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextPrimary import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled import com.vitorpamplona.amethyst.commons.ui.components.NowhereLinkCard diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentStateView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentStateView.kt index ce29dc0daf..85dc9c6610 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentStateView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentStateView.kt @@ -35,6 +35,8 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox +import com.vitorpamplona.amethyst.commons.ui.feeds.StickToTopOnPrepend +import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverLazyGridState import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverLazyListState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/WatchScrollToTop.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/WatchScrollToTop.kt index b9580ea8ba..d0d7daa8c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/WatchScrollToTop.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/WatchScrollToTop.kt @@ -21,52 +21,11 @@ package com.vitorpamplona.amethyst.ui.feeds import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.grid.LazyGridState -import androidx.compose.foundation.pager.PagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshotFlow import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.commons.feeds.FeedContentState -import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedContentState -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map - -@Composable -fun WatchScrollToTop( - feedContentState: FeedContentState, - listState: LazyListState, -) { - val scrollToTop by feedContentState.scrollToTop.collectAsStateWithLifecycle() - - LaunchedEffect(scrollToTop) { - if (scrollToTop > 0 && feedContentState.scrollToTopPending) { - listState.scrollToItem(index = 0) - feedContentState.sentToTop() - } - } -} - -@Composable -fun WatchScrollToTop( - feedContentState: FeedContentState, - listState: LazyGridState, -) { - val scrollToTop by feedContentState.scrollToTop.collectAsStateWithLifecycle() - - LaunchedEffect(scrollToTop) { - if (scrollToTop > 0 && feedContentState.scrollToTopPending) { - listState.scrollToItem(index = 0) - feedContentState.sentToTop() - } - } -} @Composable fun WatchScrollToTop( @@ -82,166 +41,3 @@ fun WatchScrollToTop( } } } - -@Composable -fun WatchScrollToTop( - videoFeedContentState: FeedContentState, - pagerState: PagerState, -) { - val scrollToTop by videoFeedContentState.scrollToTop.collectAsStateWithLifecycle() - - LaunchedEffect(scrollToTop) { - if (scrollToTop > 0 && videoFeedContentState.scrollToTopPending) { - pagerState.scrollToPage(page = 0) - videoFeedContentState.sentToTop() - } - } -} - -/** - * Keeps the user pinned to index 0 when new items prepend to a feed, but - * only if they were already at the very top right before the update. - * - * Why this is non-trivial: every feed uses stable `key = item.idHex` in - * its lazy list, which makes Compose preserve the visual anchor across - * data changes. When N items prepend, the user's previously-visible - * top item is still on screen but its index is now N — so - * `firstVisibleItemIndex` shifts from 0 to N without any user gesture. - * A naive `if (firstVisibleItemIndex <= 1) scrollToItem(0)` check inside - * `LaunchedEffect(items.firstOrNull())` therefore fails as soon as more - * than one item arrives in the same batch. - * - * The trick: track "was at top" continuously via [snapshotFlow], but - * only flip it from true → false when [LazyListState.isScrollInProgress] - * is true (i.e. the user is actively scrolling). Compose's keyed-item - * shift after a data update does not set that flag — only real touch - * gestures and `animate*` calls do — so data-driven index shifts can - * never poison the cached value. When [firstItemKey] changes (head of - * the list moved), if the cached value is still true, snap back to 0 - * with an instant (non-animated) scroll so the prepend appears as - * in-place growth rather than a visible jump-then-scroll. - * - * Most callers should not invoke this directly: [SaveableFeedContentState], - * [SaveableGridFeedContentState], and the analogous wrappers in - * `ui/screen/FeedView.kt` already apply auto-stick to every feed they - * own. Invoke the explicit overload only when the listState is - * constructed outside one of those wrappers, or when the key that - * should trigger the snap is not the default `items.list[0].idHex` - * (e.g. notifications, chats, or feeds keyed on something other than a - * Note's hex id). - */ -@Composable -fun StickToTopOnPrepend( - listState: LazyListState, - firstItemKey: Any?, -) { - stickToTopOnPrepend( - stateKey = listState, - firstItemKey = firstItemKey, - initialAtTop = { - listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0 - }, - sampler = { - snapshotFlow { - listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0 - } - }, - isScrollInProgress = { listState.isScrollInProgress }, - firstVisibleItemIndex = { listState.firstVisibleItemIndex }, - scrollToTop = { listState.scrollToItem(0) }, - ) -} - -@Composable -fun StickToTopOnPrepend( - gridState: LazyGridState, - firstItemKey: Any?, -) { - stickToTopOnPrepend( - stateKey = gridState, - firstItemKey = firstItemKey, - initialAtTop = { - gridState.firstVisibleItemIndex == 0 && gridState.firstVisibleItemScrollOffset == 0 - }, - sampler = { - snapshotFlow { - gridState.firstVisibleItemIndex == 0 && gridState.firstVisibleItemScrollOffset == 0 - } - }, - isScrollInProgress = { gridState.isScrollInProgress }, - firstVisibleItemIndex = { gridState.firstVisibleItemIndex }, - scrollToTop = { gridState.scrollToItem(0) }, - ) -} - -/** - * Auto-stick wired straight to a [FeedContentState]: derives the head - * key from `feedContent → Loaded.feed → list.firstOrNull()?.idHex` so - * callers don't have to collect the inner feed flow themselves. Used - * by the Saveable* wrappers; suitable for any Note-keyed feed. - */ -@Composable -fun StickToTopOnPrepend( - feedContentState: FeedContentState, - listState: LazyListState, -) { - StickToTopOnPrepend(listState, rememberFirstItemIdHex(feedContentState)) -} - -@Composable -fun StickToTopOnPrepend( - feedContentState: FeedContentState, - gridState: LazyGridState, -) { - StickToTopOnPrepend(gridState, rememberFirstItemIdHex(feedContentState)) -} - -@OptIn(ExperimentalCoroutinesApi::class) -@Composable -private fun rememberFirstItemIdHex(feedContentState: FeedContentState): String? { - val flow = - remember(feedContentState) { - feedContentState.feedContent.flatMapLatest { state -> - when (state) { - is FeedState.Loaded -> state.feed.map { it.list.firstOrNull()?.idHex } - else -> flowOf(null) - } - } - } - val key by flow.collectAsStateWithLifecycle(initialValue = null) - return key -} - -@Composable -private fun stickToTopOnPrepend( - stateKey: Any, - firstItemKey: Any?, - initialAtTop: () -> Boolean, - sampler: () -> Flow<Boolean>, - isScrollInProgress: () -> Boolean, - firstVisibleItemIndex: () -> Int, - scrollToTop: suspend () -> Unit, -) { - // Plain holder instead of mutableStateOf — we only read this inside - // effects, never in composition, so we don't need snapshot tracking. - // Seed from the actual restored scroll position: when the user returns - // to a feed via rememberForeverLazyListState, the saved offset is - // already in place, and a hardcoded `true` would mis-snap them to 0. - val wasAtTop = remember(stateKey) { booleanArrayOf(initialAtTop()) } - - LaunchedEffect(stateKey) { - sampler().collect { atTop -> - if (atTop) { - wasAtTop[0] = true - } else if (isScrollInProgress()) { - wasAtTop[0] = false - } - } - } - - LaunchedEffect(firstItemKey) { - if (firstItemKey != null && wasAtTop[0] && firstVisibleItemIndex() > 0) { - scrollToTop() - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayout.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayoutPreview.kt similarity index 77% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayout.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayoutPreview.kt index 5abf8308cf..1f49f5ac52 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayout.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayoutPreview.kt @@ -20,16 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.layouts -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.ListItem import androidx.compose.material3.Text @@ -38,20 +32,19 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.profile_banner +import com.vitorpamplona.amethyst.commons.ui.components.NewItemsBubble +import com.vitorpamplona.amethyst.commons.ui.layouts.ChatHeaderLayout +import com.vitorpamplona.amethyst.commons.ui.layouts.listItem.SlimListItem import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness -import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpModifier import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn -import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.ui.painterRes -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.NewItemsBubble import com.vitorpamplona.quartz.utils.TimeUtils private const val PREVIEW_AUTHOR = "This is my author" @@ -136,43 +129,3 @@ fun ChannelNamePreview() { } } } - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun ChatHeaderLayout( - channelPicture: @Composable () -> Unit, - firstRow: @Composable RowScope.() -> Unit, - secondRow: @Composable RowScope.() -> Unit, - onClick: () -> Unit, - onLongClick: (() -> Unit)? = null, -) { - Row( - modifier = - Modifier - .combinedClickable(onClick = onClick, onLongClick = onLongClick) - .padding(10.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Box(Size55Modifier) { channelPicture() } - - Spacer(modifier = DoubleHorzSpacer) - - Column( - modifier = Modifier.fillMaxWidth(), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - firstRow() - } - - Spacer(modifier = Height4dpModifier) - - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - secondRow() - } - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/LeftPictureLayout.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/LeftPictureLayoutPreview.kt similarity index 72% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/LeftPictureLayout.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/LeftPictureLayoutPreview.kt index 167bbc4a51..2f26c2e73e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/LeftPictureLayout.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/LeftPictureLayoutPreview.kt @@ -21,18 +21,11 @@ package com.vitorpamplona.amethyst.ui.layouts import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.MaterialTheme 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.clip import androidx.compose.ui.layout.ContentScale @@ -43,10 +36,10 @@ import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.profile_banner +import com.vitorpamplona.amethyst.commons.ui.layouts.LeftPictureLayout import com.vitorpamplona.amethyst.commons.ui.note.LikeIcon import com.vitorpamplona.amethyst.commons.ui.note.ZappedIcon import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.commons.ui.theme.Size16Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier @@ -104,47 +97,3 @@ fun LeftPictureLayoutPreviewCard() { onBottomRow = { Text("This is my Moderator List") }, ) } - -@Composable -fun LeftPictureLayout( - onImage: @Composable () -> Unit, - onTitleRow: @Composable RowScope.() -> Unit, - onDescription: @Composable () -> Unit, - onBottomRow: @Composable RowScope.() -> Unit, - imageFraction: Float = 0.25f, -) { - Row(Modifier.aspectRatio(ratio = 1 / imageFraction)) { - Column( - modifier = Modifier.fillMaxWidth(imageFraction).aspectRatio(ratio = 1f), - ) { - onImage() - } - - Spacer(modifier = DoubleHorzSpacer) - - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.SpaceBetween, - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - onTitleRow() - } - - Row( - modifier = Modifier.fillMaxWidth().weight(1f), - ) { - onDescription() - } - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - onBottomRow() - } - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/listItem/SlimListItemPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/listItem/SlimListItemPreview.kt new file mode 100644 index 0000000000..f4529fc097 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/listItem/SlimListItemPreview.kt @@ -0,0 +1,132 @@ +/* + * 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.layouts.listItem + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ListItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.tooling.preview.Preview +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.profile_banner +import com.vitorpamplona.amethyst.commons.ui.components.NewItemsBubble +import com.vitorpamplona.amethyst.commons.ui.layouts.ChatHeaderLayout +import com.vitorpamplona.amethyst.commons.ui.layouts.listItem.SlimListItem +import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpModifier +import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier +import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn +import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo +import com.vitorpamplona.amethyst.ui.painterRes +import com.vitorpamplona.quartz.utils.TimeUtils + +private const val PREVIEW_AUTHOR = "This is my author" +private const val PREVIEW_MESSAGE = "This is a message from this person" + +@Composable +@Preview +fun ChannelNamePreview() { + ThemeComparisonColumn { + Column { + ChatHeaderLayout( + channelPicture = { + Image( + painter = painterRes(R.drawable.github, 1), + contentDescription = stringRes(id = Res.string.profile_banner), + contentScale = ContentScale.FillWidth, + ) + }, + firstRow = { + Text(PREVIEW_AUTHOR, Modifier.weight(1f)) + TimeAgo(TimeUtils.now()) + }, + secondRow = { + Text(PREVIEW_MESSAGE, Modifier.weight(1f)) + NewItemsBubble() + }, + onClick = {}, + ) + + HorizontalDivider(thickness = DividerThickness) + + SlimListItem( + headlineContent = { + Row(verticalAlignment = CenterVertically) { + Text(PREVIEW_AUTHOR, Modifier.weight(1f)) + TimeAgo(TimeUtils.now()) + } + }, + supportingContent = { + Row(verticalAlignment = CenterVertically) { + Text(PREVIEW_MESSAGE, Modifier.weight(1f)) + Spacer(modifier = Height4dpModifier) + NewItemsBubble() + } + }, + leadingContent = { + Image( + painter = painterRes(R.drawable.github, 2), + contentDescription = stringRes(id = Res.string.profile_banner), + contentScale = ContentScale.FillWidth, + modifier = Size55Modifier, + ) + }, + ) + + HorizontalDivider(thickness = DividerThickness) + + ListItem( + headlineContent = { + Row(verticalAlignment = CenterVertically) { + Text(PREVIEW_AUTHOR, Modifier.weight(1f)) + TimeAgo(TimeUtils.now()) + } + }, + supportingContent = { + Row(verticalAlignment = CenterVertically) { + Text(PREVIEW_MESSAGE, Modifier.weight(1f)) + Spacer(modifier = Height4dpModifier) + NewItemsBubble() + } + }, + leadingContent = { + Image( + painter = painterRes(R.drawable.github, 2), + contentDescription = stringRes(id = Res.string.profile_banner), + contentScale = ContentScale.FillWidth, + modifier = Size55Modifier, + ) + }, + ) + + HorizontalDivider(thickness = DividerThickness) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt index 46771e5f29..f002b59bef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt @@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.resources.follow import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_follow import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_unfollow import com.vitorpamplona.amethyst.commons.resources.read_only_user +import com.vitorpamplona.amethyst.commons.ui.layouts.listItem.SlimListItem import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.ShowUserButton import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp @@ -51,7 +52,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAboutMe import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowing -import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/AudienceSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/AudienceSheet.kt index bb78a2b1b1..e2aedac116 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/AudienceSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/notify/AudienceSheet.kt @@ -88,6 +88,7 @@ import com.vitorpamplona.amethyst.commons.resources.discover_follows import com.vitorpamplona.amethyst.commons.resources.follow_sets import com.vitorpamplona.amethyst.commons.resources.num_selected import com.vitorpamplona.amethyst.commons.resources.select_all +import com.vitorpamplona.amethyst.commons.ui.components.OutlinedThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness @@ -95,7 +96,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size24dp import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.warningColor -import com.vitorpamplona.amethyst.ui.components.OutlinedThinPaddingTextField import com.vitorpamplona.amethyst.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt index b13c5e1386..ee8e180270 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt @@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.user_suggestion_in_this_chat +import com.vitorpamplona.amethyst.commons.ui.layouts.listItem.SlimListItem import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.Font14SP @@ -60,7 +61,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.NIP05IconSize import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp import com.vitorpamplona.amethyst.commons.ui.theme.nip05 import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription -import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.ObserveAndRenderNIP05VerifiedSymbol import com.vitorpamplona.amethyst.ui.note.UsernameDisplay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/ForwardZapTo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/ForwardZapTo.kt index 6554cdcabe..ad2b1c3ea5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/ForwardZapTo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/ForwardZapTo.kt @@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.commons.resources.zap_split_explainer import com.vitorpamplona.amethyst.commons.resources.zap_split_search_and_add_user import com.vitorpamplona.amethyst.commons.resources.zap_split_search_and_add_user_placeholder import com.vitorpamplona.amethyst.commons.resources.zap_split_title +import com.vitorpamplona.amethyst.commons.ui.components.OutlinedThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.IZapField import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ZapSplitIcon import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -55,7 +56,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation -import com.vitorpamplona.amethyst.ui.components.OutlinedThinPaddingTextField import com.vitorpamplona.amethyst.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index 421549ffb6..bcb73b461a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -35,14 +35,14 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox +import com.vitorpamplona.amethyst.commons.ui.feeds.StickToTopOnPrepend +import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverLazyGridState import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverLazyListState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.viewmodels.FeedViewModel import com.vitorpamplona.amethyst.ui.feeds.FeedLoaded import com.vitorpamplona.amethyst.ui.feeds.PrefetchLoadedFeedMedia -import com.vitorpamplona.amethyst.ui.feeds.StickToTopOnPrepend -import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt index 880e12de22..4ed610bed1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt @@ -35,11 +35,11 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox +import com.vitorpamplona.amethyst.commons.ui.feeds.StickToTopOnPrepend import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding -import com.vitorpamplona.amethyst.ui.feeds.StickToTopOnPrepend import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt index dd46388f4d..a62a265f18 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt @@ -22,13 +22,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn import android.content.ComponentCallbacks2 import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.feeds.ChannelFeedContentState import com.vitorpamplona.amethyst.commons.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState import com.vitorpamplona.amethyst.ui.screen.loggedIn.articles.dal.ArticlesFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.dal.BadgesFeedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt index 644d10c45e..c84bbfbd80 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarCollectionsView.kt @@ -64,13 +64,13 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_export_event import com.vitorpamplona.amethyst.commons.resources.calendar_export_share_title import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.commons.ui.feeds.ScrollStateKeys +import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverLazyListState import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding -import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.UserCardHeader import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarFeedView.kt index 443c0b0b0d..0d62fc715b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarFeedView.kt @@ -47,11 +47,11 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_empty_feed_title import com.vitorpamplona.amethyst.commons.resources.calendar_section_past import com.vitorpamplona.amethyst.commons.resources.calendar_section_upcoming import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox +import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding -import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 8b2ba48747..f758b7bdcc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -106,7 +106,9 @@ import com.vitorpamplona.amethyst.commons.resources.relay_group_server_label import com.vitorpamplona.amethyst.commons.resources.remove_from_messages import com.vitorpamplona.amethyst.commons.resources.unmute_notifications import com.vitorpamplona.amethyst.commons.resources.unpin_conversation +import com.vitorpamplona.amethyst.commons.ui.components.NewItemsBubble import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage +import com.vitorpamplona.amethyst.commons.ui.layouts.ChatHeaderLayout import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.HeaderPill import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings @@ -118,7 +120,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size15Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.grayText -import com.vitorpamplona.amethyst.commons.ui.theme.newItemBubbleModifier import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.model.buzz.toMembershipNotice import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo @@ -126,7 +127,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observe import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteHasEvent import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderByParentFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName -import com.vitorpamplona.amethyst.ui.layouts.ChatHeaderLayout import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.BlankNote import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContentOrNull @@ -1377,8 +1377,3 @@ private fun TimeAgo(channelLastTime: Long?) { color = MaterialTheme.colorScheme.grayText, ) } - -@Composable -fun NewItemsBubble() { - Box(MaterialTheme.colorScheme.newItemBubbleModifier) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt index 4623df34fe..fda82b5723 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt @@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.Mute import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.chat_about_topic +import com.vitorpamplona.amethyst.commons.ui.layouts.LeftPictureLayout import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.HalfTopPadding @@ -61,7 +62,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.RowColSpacing import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel -import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.ui.note.Gallery import com.vitorpamplona.amethyst.ui.note.LikeReaction diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt index 0a4dc47515..43dfdebcbe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt @@ -55,6 +55,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.Mute import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.community_about_topic +import com.vitorpamplona.amethyst.commons.ui.layouts.LeftPictureLayout import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.HalfTopPadding @@ -63,7 +64,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.RowColSpacing import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent -import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.ui.note.Gallery import com.vitorpamplona.amethyst.ui.note.LikeReaction diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt index 64b35607fc..89efcd5e61 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt @@ -41,6 +41,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.ui.layouts.LeftPictureLayout import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.theme.HalfTopPadding import com.vitorpamplona.amethyst.commons.ui.theme.RowColSpacing5dp @@ -51,7 +52,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.ui.theme.nip05 import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.components.MyAsyncImage -import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.note.elements.BannerImage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/browse/BrowseEmojiSetsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/browse/BrowseEmojiSetsScreen.kt index 4aa7c25131..ddcb0b073f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/browse/BrowseEmojiSetsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/browse/BrowseEmojiSetsScreen.kt @@ -46,12 +46,12 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.commons.ui.feeds.ScrollStateKeys +import com.vitorpamplona.amethyst.commons.ui.feeds.StickToTopOnPrepend import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel +import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverLazyGridState import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav -import com.vitorpamplona.amethyst.ui.feeds.StickToTopOnPrepend -import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index 66ebccef97..14b4ff9783 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -61,6 +61,8 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.feeds.ChannelFeedContentState +import com.vitorpamplona.amethyst.commons.feeds.ChannelFeedState import com.vitorpamplona.amethyst.commons.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel @@ -96,8 +98,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.TabRowHeight import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.service.location.LocationState -import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState -import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedState import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt index e471e8f3c7..bec6b7c1fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt @@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachCursor import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachDetailDialog import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachMarkers +import com.vitorpamplona.amethyst.commons.ui.feeds.StickToTopOnPrepend import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.CloseIcon @@ -73,7 +74,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier import com.vitorpamplona.amethyst.logTime -import com.vitorpamplona.amethyst.ui.feeds.StickToTopOnPrepend import com.vitorpamplona.amethyst.ui.note.BadgeCompose import com.vitorpamplona.amethyst.ui.note.MessageSetCompose import com.vitorpamplona.amethyst.ui.note.MultiSetCompose diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt index cd772a90de..208c12bb85 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt @@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.commons.resources.notification_tab_following import com.vitorpamplona.amethyst.commons.ui.feeds.PagerStateKeys import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.commons.ui.feeds.ScrollStateKeys +import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverLazyListState import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverPagerState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/TabNotesNewThreads.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/TabNotesNewThreads.kt index abed0924d7..d600bd16fd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/TabNotesNewThreads.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/TabNotesNewThreads.kt @@ -40,12 +40,12 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox +import com.vitorpamplona.amethyst.commons.ui.feeds.StickToTopOnPrepend +import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.feeds.PrefetchLoadedFeedMedia -import com.vitorpamplona.amethyst.ui.feeds.StickToTopOnPrepend -import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.dal.UserProfileNewThreadsFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapNoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapNoteCompose.kt index ed93534498..f3d45d8565 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapNoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapNoteCompose.kt @@ -35,12 +35,12 @@ import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.routeFor +import com.vitorpamplona.amethyst.commons.ui.layouts.listItem.SlimListItem import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn -import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.UserActionOptions import com.vitorpamplona.amethyst.ui.note.UserPicture diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt index fd0e1f5a7c..0498e1996f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt @@ -122,6 +122,7 @@ import com.vitorpamplona.amethyst.commons.resources.relay_management_tab_pubkeys import com.vitorpamplona.amethyst.commons.resources.relay_management_tab_settings import com.vitorpamplona.amethyst.commons.resources.relay_management_title import com.vitorpamplona.amethyst.commons.resources.search_and_add_a_user +import com.vitorpamplona.amethyst.commons.ui.layouts.listItem.SlimListItem import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.ClearTextIcon import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -133,7 +134,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SmallBorder import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.nip05 import com.vitorpamplona.amethyst.service.relayClient.searchCommand.UserSearchDataSourceSubscription -import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem import com.vitorpamplona.amethyst.ui.note.AboutDisplay import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.ObserveAndRenderNIP05VerifiedSymbol diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/ChannelFeedContentState.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/ChannelFeedContentState.kt index 611d27ff59..296893376c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/ChannelFeedContentState.kt @@ -18,26 +18,24 @@ * 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.feeds +package com.vitorpamplona.amethyst.commons.feeds import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf -import com.vitorpamplona.amethyst.commons.feeds.AdditiveComplexFeedFilter -import com.vitorpamplona.amethyst.commons.feeds.InvalidatableContent -import com.vitorpamplona.amethyst.commons.feeds.LoadedFeedState import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.service.BundledInsert import com.vitorpamplona.amethyst.commons.service.BundledUpdate -import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists +import com.vitorpamplona.amethyst.commons.util.equalImmutableLists import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.flattenToSet import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @@ -75,7 +73,7 @@ class ChannelFeedContentState( } fun refreshSuspended() { - checkNotInMainThread() + LocalCache.appHost.assertNotMainThread() isRefreshing.value = true try { @@ -178,7 +176,7 @@ class ChannelFeedContentState( } fun destroy() { - Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } + Log.d("Init") { "OnCleared: ${this::class.simpleName}" } bundlerInsert.cancel() bundler.cancel() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/ChannelFeedState.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedState.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/ChannelFeedState.kt index be96af6803..b3cb58b1e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/ChannelFeedState.kt @@ -18,10 +18,9 @@ * 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.feeds +package com.vitorpamplona.amethyst.commons.feeds import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.commons.feeds.LoadedFeedState import com.vitorpamplona.amethyst.commons.model.Channel import kotlinx.coroutines.flow.MutableStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableBuzzInviteLink.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ClickableBuzzInviteLink.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableBuzzInviteLink.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ClickableBuzzInviteLink.kt index 7779422d4b..089ff137a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableBuzzInviteLink.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ClickableBuzzInviteLink.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.components +package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.foundation.combinedClickable import androidx.compose.material3.MaterialTheme diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableConcordInviteLink.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ClickableConcordInviteLink.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableConcordInviteLink.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ClickableConcordInviteLink.kt index 3212e52163..e18ff60ff8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableConcordInviteLink.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ClickableConcordInviteLink.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.components +package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.foundation.combinedClickable import androidx.compose.material3.MaterialTheme diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ClickableLinks.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ClickableLinks.kt index beedb404fc..8a48da7f7d 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ClickableLinks.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ClickableLinks.kt @@ -91,3 +91,27 @@ fun ClickableEmail( }, ) } + +/** + * A primary-colored clickable phone number that opens the platform dialer via + * [LocalUriHandler] (`tel:`). Strips a leading `tel:` from the display text. + * [underline] + hover cursor are the Desktop mouse-first affordances. + */ +@Composable +fun ClickablePhone( + number: String, + modifier: Modifier = Modifier, + underline: Boolean = false, +) { + val uriHandler = LocalUriHandler.current + val display = remember(number) { number.removePrefix("tel:") } + Text( + text = display, + color = MaterialTheme.colorScheme.primary, + textDecoration = if (underline) TextDecoration.Underline else null, + modifier = + modifier.pointerHoverIcon(PointerIcon.Hand).clickable { + runCatching { uriHandler.openUri("tel:$display") } + }, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRelayGroupLink.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ClickableRelayGroupLink.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRelayGroupLink.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ClickableRelayGroupLink.kt index d968ab300b..72a1104bed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRelayGroupLink.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ClickableRelayGroupLink.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.components +package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.foundation.combinedClickable import androidx.compose.material3.MaterialTheme diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRelayUrl.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ClickableRelayUrl.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRelayUrl.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ClickableRelayUrl.kt index 303fd67251..60e3668e5c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRelayUrl.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ClickableRelayUrl.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.components +package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.foundation.combinedClickable import androidx.compose.material3.MaterialTheme diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickablePhone.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/NewItemsBubble.kt similarity index 63% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickablePhone.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/NewItemsBubble.kt index 3eb265a4e7..0d7b070240 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickablePhone.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/NewItemsBubble.kt @@ -18,30 +18,15 @@ * 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 +package com.vitorpamplona.amethyst.commons.ui.components -import android.content.Context -import android.content.Intent -import android.net.Uri +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextPrimary +import com.vitorpamplona.amethyst.commons.ui.theme.newItemBubbleModifier +/** The small dot that marks a row with unread items. */ @Composable -fun ClickablePhone(phone: String) { - val context = LocalContext.current - - ClickableTextPrimary( - text = phone, - onClick = { context.dial(phone) }, - ) -} - -fun Context.dial(phone: String) { - try { - val intent = Intent(Intent.ACTION_DIAL, Uri.fromParts("tel", phone, null)) - startActivity(intent) - } catch (t: Throwable) { - // TODO: Handle potential exceptions - } +fun NewItemsBubble() { + Box(MaterialTheme.colorScheme.newItemBubbleModifier) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/OutlinedThinPaddingTextField.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/OutlinedThinPaddingTextField.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/OutlinedThinPaddingTextField.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/OutlinedThinPaddingTextField.kt index 3ee7f3cc0a..d7e7a537ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/OutlinedThinPaddingTextField.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/OutlinedThinPaddingTextField.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.components +package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.interaction.MutableInteractionSource diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/StickToTopOnPrepend.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/StickToTopOnPrepend.kt index 9074a494dd..7ba0fb5eed 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/StickToTopOnPrepend.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/StickToTopOnPrepend.kt @@ -24,10 +24,10 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.snapshotFlow +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.feeds.FeedState import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -59,11 +59,14 @@ import kotlinx.coroutines.flow.map * with an instant (non-animated) scroll so the prepend appears as * in-place growth rather than a visible jump-then-scroll. * - * Commons port of `amethyst/.../WatchScrollToTop.kt` so Desktop and any - * other multiplatform front-end can use the same auto-stick behavior. - * Uses plain `collectAsState` instead of the Android-only - * `collectAsStateWithLifecycle` — equivalent here because the effect's - * lifecycle is already bound to composition via `LaunchedEffect`. + * Most callers should not invoke this directly: `SaveableFeedContentState`, + * `SaveableGridFeedContentState`, and the analogous wrappers in the app's + * `ui/screen/FeedView.kt` already apply auto-stick to every feed they + * own. Invoke the explicit overload only when the listState is + * constructed outside one of those wrappers, or when the key that + * should trigger the snap is not the default `items.list[0].idHex` + * (e.g. notifications, chats, or feeds keyed on something other than a + * Note's hex id). */ @Composable fun StickToTopOnPrepend( @@ -142,7 +145,7 @@ private fun rememberFirstItemIdHex(feedContentState: FeedContentState): String? } } } - val key by flow.collectAsState(initial = null) + val key by flow.collectAsStateWithLifecycle(initialValue = null) return key } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/WatchScrollToTop.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/WatchScrollToTop.kt new file mode 100644 index 0000000000..5bd267e8f5 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/feeds/WatchScrollToTop.kt @@ -0,0 +1,75 @@ +/* + * 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.commons.ui.feeds + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.pager.PagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.feeds.FeedContentState + +@Composable +fun WatchScrollToTop( + feedContentState: FeedContentState, + listState: LazyListState, +) { + val scrollToTop by feedContentState.scrollToTop.collectAsStateWithLifecycle() + + LaunchedEffect(scrollToTop) { + if (scrollToTop > 0 && feedContentState.scrollToTopPending) { + listState.scrollToItem(index = 0) + feedContentState.sentToTop() + } + } +} + +@Composable +fun WatchScrollToTop( + feedContentState: FeedContentState, + listState: LazyGridState, +) { + val scrollToTop by feedContentState.scrollToTop.collectAsStateWithLifecycle() + + LaunchedEffect(scrollToTop) { + if (scrollToTop > 0 && feedContentState.scrollToTopPending) { + listState.scrollToItem(index = 0) + feedContentState.sentToTop() + } + } +} + +@Composable +fun WatchScrollToTop( + videoFeedContentState: FeedContentState, + pagerState: PagerState, +) { + val scrollToTop by videoFeedContentState.scrollToTop.collectAsStateWithLifecycle() + + LaunchedEffect(scrollToTop) { + if (scrollToTop > 0 && videoFeedContentState.scrollToTopPending) { + pagerState.scrollToPage(page = 0) + videoFeedContentState.sentToTop() + } + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/ChatHeaderLayout.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/ChatHeaderLayout.kt new file mode 100644 index 0000000000..76fc5b6425 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/ChatHeaderLayout.kt @@ -0,0 +1,78 @@ +/* + * 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.commons.ui.layouts + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer +import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpModifier +import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun ChatHeaderLayout( + channelPicture: @Composable () -> Unit, + firstRow: @Composable RowScope.() -> Unit, + secondRow: @Composable RowScope.() -> Unit, + onClick: () -> Unit, + onLongClick: (() -> Unit)? = null, +) { + Row( + modifier = + Modifier + .combinedClickable(onClick = onClick, onLongClick = onLongClick) + .padding(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box(Size55Modifier) { channelPicture() } + + Spacer(modifier = DoubleHorzSpacer) + + Column( + modifier = Modifier.fillMaxWidth(), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + firstRow() + } + + Spacer(modifier = Height4dpModifier) + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + secondRow() + } + } + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/LeftPictureLayout.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/LeftPictureLayout.kt new file mode 100644 index 0000000000..2d7102877c --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/LeftPictureLayout.kt @@ -0,0 +1,77 @@ +/* + * 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.commons.ui.layouts + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer + +@Composable +fun LeftPictureLayout( + onImage: @Composable () -> Unit, + onTitleRow: @Composable RowScope.() -> Unit, + onDescription: @Composable () -> Unit, + onBottomRow: @Composable RowScope.() -> Unit, + imageFraction: Float = 0.25f, +) { + Row(Modifier.aspectRatio(ratio = 1 / imageFraction)) { + Column( + modifier = Modifier.fillMaxWidth(imageFraction).aspectRatio(ratio = 1f), + ) { + onImage() + } + + Spacer(modifier = DoubleHorzSpacer) + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.SpaceBetween, + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + onTitleRow() + } + + Row( + modifier = Modifier.fillMaxWidth().weight(1f), + ) { + onDescription() + } + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + onBottomRow() + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/listItem/SlimListItemLayout.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/listItem/SlimListItemLayout.kt similarity index 81% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/listItem/SlimListItemLayout.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/listItem/SlimListItemLayout.kt index ed6f0b1a5d..b7ac7505f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/listItem/SlimListItemLayout.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/listItem/SlimListItemLayout.kt @@ -18,31 +18,22 @@ * 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.layouts.listItem +package com.vitorpamplona.amethyst.commons.ui.layouts.listItem -import androidx.annotation.VisibleForTesting -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemColors import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.FirstBaseline import androidx.compose.ui.layout.IntrinsicMeasurable import androidx.compose.ui.layout.IntrinsicMeasureScope @@ -55,117 +46,20 @@ import androidx.compose.ui.layout.MultiContentMeasurePolicy import androidx.compose.ui.layout.Placeable import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.offset import androidx.compose.ui.unit.sp -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.resources.Res -import com.vitorpamplona.amethyst.commons.resources.profile_banner -import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness -import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpModifier -import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier -import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn -import com.vitorpamplona.amethyst.ui.layouts.ChatHeaderLayout -import com.vitorpamplona.amethyst.ui.layouts.listItem.ListTokens.ListItemContainerElevation -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo -import com.vitorpamplona.amethyst.ui.painterRes -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.NewItemsBubble -import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.amethyst.commons.ui.layouts.listItem.ListTokens.ListItemContainerElevation +import kotlin.jvm.JvmInline import kotlin.math.max /** * This is a copy of Material3's ListItemLayout.kt file, with the only change being the padding change from 16.dp to 10.dp */ -private const val PREVIEW_AUTHOR = "This is my author" -private const val PREVIEW_MESSAGE = "This is a message from this person" - -@Composable -@Preview -fun ChannelNamePreview() { - ThemeComparisonColumn { - Column { - ChatHeaderLayout( - channelPicture = { - Image( - painter = painterRes(R.drawable.github, 1), - contentDescription = stringRes(id = Res.string.profile_banner), - contentScale = ContentScale.FillWidth, - ) - }, - firstRow = { - Text(PREVIEW_AUTHOR, Modifier.weight(1f)) - TimeAgo(TimeUtils.now()) - }, - secondRow = { - Text(PREVIEW_MESSAGE, Modifier.weight(1f)) - NewItemsBubble() - }, - onClick = {}, - ) - - HorizontalDivider(thickness = DividerThickness) - - SlimListItem( - headlineContent = { - Row(verticalAlignment = CenterVertically) { - Text(PREVIEW_AUTHOR, Modifier.weight(1f)) - TimeAgo(TimeUtils.now()) - } - }, - supportingContent = { - Row(verticalAlignment = CenterVertically) { - Text(PREVIEW_MESSAGE, Modifier.weight(1f)) - Spacer(modifier = Height4dpModifier) - NewItemsBubble() - } - }, - leadingContent = { - Image( - painter = painterRes(R.drawable.github, 2), - contentDescription = stringRes(id = Res.string.profile_banner), - contentScale = ContentScale.FillWidth, - modifier = Size55Modifier, - ) - }, - ) - - HorizontalDivider(thickness = DividerThickness) - - ListItem( - headlineContent = { - Row(verticalAlignment = CenterVertically) { - Text(PREVIEW_AUTHOR, Modifier.weight(1f)) - TimeAgo(TimeUtils.now()) - } - }, - supportingContent = { - Row(verticalAlignment = CenterVertically) { - Text(PREVIEW_MESSAGE, Modifier.weight(1f)) - Spacer(modifier = Height4dpModifier) - NewItemsBubble() - } - }, - leadingContent = { - Image( - painter = painterRes(R.drawable.github, 2), - contentDescription = stringRes(id = Res.string.profile_banner), - contentScale = ContentScale.FillWidth, - modifier = Size55Modifier, - ) - }, - ) - - HorizontalDivider(thickness = DividerThickness) - } - } -} - @Composable fun SlimListItem( headlineContent: @Composable () -> Unit, @@ -688,22 +582,22 @@ object ListTokens { // Container related defaults // TODO: Make sure these values stay up to date until replaced with tokens. -@VisibleForTesting internal val ListItemVerticalPadding = 8.dp +internal val ListItemVerticalPadding = 8.dp -@VisibleForTesting internal val ListItemThreeLineVerticalPadding = 12.dp +internal val ListItemThreeLineVerticalPadding = 12.dp -@VisibleForTesting internal val ListItemStartPadding = 10.dp +internal val ListItemStartPadding = 10.dp -@VisibleForTesting internal val ListItemEndPadding = 10.dp +internal val ListItemEndPadding = 10.dp // Icon related defaults. // TODO: Make sure these values stay up to date until replaced with tokens. -@VisibleForTesting internal val LeadingContentEndPadding = Modifier.padding(end = 10.dp) +internal val LeadingContentEndPadding = Modifier.padding(end = 10.dp) // Icon related defaults. // TODO: Make sure these values stay up to date until replaced with tokens. -@VisibleForTesting internal val SupportingContentTopPadding = Modifier.padding(top = 2.dp) +internal val SupportingContentTopPadding = Modifier.padding(top = 2.dp) // Trailing related defaults. // TODO: Make sure these values stay up to date until replaced with tokens. -@VisibleForTesting internal val TrailingContentStartPadding = Modifier.padding(start = 10.dp) +internal val TrailingContentStartPadding = Modifier.padding(start = 10.dp) From f1582b391b060e63515799c2d5bf10005d50f6b8 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 21:13:16 +0000 Subject: [PATCH 17/60] docs(commons): record the Tier 4 unlocks, the strings drift fix and the UI stragglers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjQQN9CgWHVtNCSWqnKoqY --- .../2026-08-30-commons-migration-sweep.md | 101 +++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/commons/plans/2026-08-30-commons-migration-sweep.md b/commons/plans/2026-08-30-commons-migration-sweep.md index 20982ddae2..d1a325d81e 100644 --- a/commons/plans/2026-08-30-commons-migration-sweep.md +++ b/commons/plans/2026-08-30-commons-migration-sweep.md @@ -771,11 +771,11 @@ Options, for the maintainer to pick: ### Next work, in recommended order (needs maintainer go-ahead per item) -- **Tier 4 unlocks** (small, mechanical): `NappletProtocolJson` +- **Tier 4 unlocks** — **done 2026-09-26**, see the last section. (Was: small, mechanical: `NappletProtocolJson` `java.util.Base64` → `kotlin.io.encoding.Base64` (frees `NappletIdentityWatch`); `ScheduledPostStore` Jackson+`java.io.File` → kotlinx-serialization+okio (frees `ScheduledPostWorkGate`). - `LargeSoftCache` stays parked (needs a WeakReference expect/actual). + `LargeSoftCache` stays parked (needs a WeakReference expect/actual).) - **Wave 2: LocalCache move-group** — **part A landed (2026-09-19)**, see the section below. Part B (repoint `desktopApp`, delete `DesktopLocalCache.kt`) is the remaining half. @@ -1302,3 +1302,100 @@ through an `ingest` helper that pins what the cache built for the lifetime of the test instance, the way a screen holds the notes it is showing in the app, and keeps the forced GC in the test that failed so the contract is asserted rather than left to the heap. + +## 2026-09-26 — Tier 4 unlocks, the strings drift, and the UI stragglers + +### Tier 4: two jvmAndroid groups promoted to `commonMain` + +- **Napplet wire protocol.** `NappletProtocolJson`, `NappletIdentityWatch` and + `NappletRequestRouter` moved to `commons/commonMain/…/napplet`. The only + pins were `java.util.Base64` and one `System.currentTimeMillis()`. Base64 is + now `kotlin.io.encoding.Base64`, decoding with `PaddingOption.PRESENT_OPTIONAL` + because `java.util.Base64.getDecoder()` accepted unpadded input and applets + may send it. The clock is `TimeUtils.now()`. +- **Scheduled posts.** `ScheduledPostStore` had already dropped Jackson for + kotlinx-serialization; what pinned it was `java.io.File` and a POSIX + `chmod`. It now takes an okio `Path` plus a `FileSystem` (default + `platformFileSystem`), and a `String` constructor so callers need no okio, + the same shape as `FileSystemNip95BlobStore`. The owner-only permission is a + new `expect fun restrictFileToOwner(path, tag)`: + `Files.setPosixFilePermissions` on jvmAndroid, POSIX `chmod(0600)` on iOS. + The temp-file swap is `FileSystem.atomicMove`, which replaces an existing + target on every platform, so the old "rename, else delete and rename again" + fallback went with it. `ScheduledPostWorkGate`, `ScheduledPostPublisher` and + `ScheduledPostNotifier` came along; none had a JVM pin of its own. + +### The strings drift, fixed + +Between the 2026-09-22 move and today, the cordn UI and the backup-conflict +review added **447 keys** to `amethyst/src/main/res` (`cordn_*`, `backup_*`), +taking it from 190 keys back to 597. All 447 moved with +`tools/strings-migrate/migrate.py` (1,131 elements over 57 locales) and the 18 +files that read them were repointed. The non-mechanical sites: + +- `stringRes(context, …)` inside `suspend` senders (`sendAttachment`, + `sendVoiceNote`) → `loadStringRes(…)`. `adminFailureText` became `suspend`; + its only caller is already in `scope.launch`. +- The backup review kept its labels as `@StringRes Int` (`DiffGroup.label`, + `ReviewRow.*Res`, `eventTypeName()`, the `countTiles`/`moneyHero`/`laneItems` + params) → `StringResource` / `PluralStringResource`. +- `rememberPresentation` resolved labels inside `remember { }` through + `LocalResources`. Compose resources have no non-suspend accessor, so the 16 + labels `presentationOf` writes into item text are resolved in composition into + a map, and the map is the `remember` key. +- `androidx.compose.ui.res.pluralStringResource` → the compose-resources one. + +Six moved keys have no reader today (`cordn_backup_restore_title`, +`cordn_coordinators_relays`, `cordn_create_admin_only_me`, +`cordn_create_coordinator`, `cordn_send`, `cordn_voice_play`). They moved +rather than being deleted; dropping them is the feature owner's call. +`pow_notification_sending` stays in Android `res/`: the PoW foreground service +reads it synchronously. + +The rule is now written down in `.claude/CLAUDE.md` ("Strings"), in +`commonsUI/ARCHITECTURE.md`, and in the `android-expert` and +`find-missing-translations` skills: new strings go in `commonsUI` +composeResources, even for Android-only screens. + +### UI stragglers moved to `commonsUI` + +A survey against `main` found that most of "Batch 4" had already landed in +other PRs (`dde228d2`, `1a563cf2`, `8dd409cb`, `1f1b6dab`): the theme +constants (in `commonsUI/…/theme/Shape.kt`, not the `Sizes.kt` this plan +named), the feed shell, the top bars, `M3ActionDialog`, `NoteComposeLayout`, +`RepostLayout` and the toast queue. This round moved what was left that had no +real blocker: + +| Moved | To | Note | +|---|---|---| +| `NewItemsBubble` (from `ChatroomHeaderCompose.kt`) | `commonsUI/…/ui/components` | | +| `ChatHeaderLayout`, `LeftPictureLayout` | `commonsUI/…/ui/layouts` | Their `@Preview`s use app drawables, `TimeAgo` and `TextCount`, so the previews stay app-side in `*Preview.kt` files | +| `listItem/SlimListItemLayout` | `commonsUI/…/ui/layouts/listItem` | Same preview split; `@VisibleForTesting` dropped (no androidx.annotation in commonMain) | +| `WatchScrollToTop` (list/grid/pager overloads) | `commonsUI/…/ui/feeds` | The `CardFeedContentState` overload stays in the app | +| app `StickToTopOnPrepend` | deleted | It was a copy of the commons one. The commons copy now collects with `collectAsStateWithLifecycle`, as the app copy did | +| `ChannelFeedState`, `ChannelFeedContentState` | `commons/commonMain/…/feeds` | `checkNotInMainThread()` → `LocalCache.appHost.assertNotMainThread()`, which Android wires to the same check | +| `ClickableBuzzInviteLink`, `ClickableConcordInviteLink`, `ClickableRelayGroupLink`, `ClickableRelayUrl`, `OutlinedThinPaddingTextField` | `commonsUI/…/ui/components` | | +| app `ClickableEmail`, `ClickablePhone` | deleted | Android now uses the commons ones (`mailto:` / `tel:` through `LocalUriHandler`) instead of `ACTION_SEND` / `ACTION_DIAL` intents. The commons `ClickablePhone` is new. Desktop still renders phone numbers as plain text, on purpose | + +**Still app-side, with the reason:** + +- `AmethystClickableIcon`: 9 lines around an Android debug action; moving it + only pushes `LocalContext` into its two callers. +- `ScreenLayout`: needs `LocalConfiguration` → `LocalWindowInfo`, and the + window-size-class breakpoints inlined. +- `SlidingCarousel`: needs `animatedViewerChromeInset` hoisted out of the + viewer-chrome file. +- `AudioWaveformReadOnly`: needs two enums from the Android-only waveform + library replaced. +- `FileAttachmentCard`: needs `extractFilename` hoisted. +- `PdfFetcher`: needs the disk cache injected. +- `ClickablePhone` / `ClickableEmail` on Desktop: deliberately unchanged. +- **`AmethystTheme` split** (the pure scheme/typography half to commonsUI; the + `Amethyst.instance` overload and the system-bar `SideEffect` stay). +- **`TimeAgo` split**: the labels, `NowProvider` and the style enum are pure; + the absolute and short formatters need expect/actual over + `android.text.format`. +- **Palette unification**: Desktop's `PlatformColorScheme` and the app palette + differ in primary, secondary, surfaces and outlines, and every shared token + (`placeholderText`, `chatBubbleThem`, …) is frozen from the app palette. This + is a design decision, not a move. From 9d0dd96de705098fad9e34e844335f6d7de0a897 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona <vitor@vitorpamplona.com> Date: Sat, 26 Sep 2026 17:41:08 -0400 Subject: [PATCH 18/60] fix(browser): the two crashes that make the new chrome unusable on a device MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both reproduce on the first page you open. Neither is visible to the build: lint, Android Lint, the metadata compiles, the native compile and the whole test suite are green with both present. **Any page with a favicon killed the full-screen browser.** `TaskDescription.Builder.setIcon(Icon)` exists from API **37**; below that the Builder only takes a drawable resource id. We compile against 37, so it resolves, and the guard was `SDK_INT >= TIRAMISU` — so Android 13, 14, 15 and 16 all called a method their framework does not have: NoSuchMethodError: No virtual method setIcon(Landroid/graphics/drawable/Icon;) at NappletBrowserActivity.updateTaskDescription(...:1186) at BrowserChromeClient.onReceivedIcon(...:520) The `runCatching` there wraps `setTaskDescription`, not the building, so it caught nothing. Gated on 37; everything below keeps the deprecated constructor, which takes the same three things and carries the bitmap anyway. Android Lint's NewApi did not flag this, which is worth knowing: compiling against a preview SDK makes its whole surface look available. **Opening the pill killed the sandbox.** The chrome is now drawn by shared composables that read `Res.string`, and those run in `:napplet`: MissingResourceException: ... Android context is not initialized. Compose Resources learns its Context from a ContentProvider the library declares, and a provider is only instantiated in the process that owns it. Three things do not work here and are worth recording so they are not retried: a second `<provider>` element (the manifest merger keys them by `android:name` and merges the two into one), a subclass with its own authority (`AndroidContextProvider` is `internal` *and* final), and `PreviewContextConfigurationEffect()`, which the exception text suggests but which sets the Context from an effect that runs *after* composition — while `stringRes` starts its async load during it. What works is the pair: `android:multiprocess="true"` lets each process hold its own instance, and acquiring the provider once in the sandbox's two activities is the first access that makes Android create it. After that every lookup resolves in-process, with no IPC. `multiprocess` alone is not enough — the instance is lazy, and nothing in `:napplet` ever addresses that authority. Verified on an SM-T220 (API 34): the full-screen browser loads a page, and the pill opens with every string, the Tor state and the site-settings summary rendered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- amethyst/src/main/AndroidManifest.xml | 16 +++++ .../napplethost/NappletBrowserActivity.kt | 13 +++- .../napplethost/NappletHostActivity.kt | 1 + .../napplethost/SandboxComposeResources.kt | 59 +++++++++++++++++++ 4 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/SandboxComposeResources.kt diff --git a/amethyst/src/main/AndroidManifest.xml b/amethyst/src/main/AndroidManifest.xml index b120f60ad1..dd42a938dd 100644 --- a/amethyst/src/main/AndroidManifest.xml +++ b/amethyst/src/main/AndroidManifest.xml @@ -640,6 +640,22 @@ android:name=".service.call.CallNotificationReceiver" android:exported="false" /> + <!-- + Compose Resources learns its Context from a ContentProvider the library + declares, and a provider is instantiated only in the process it belongs + to. The browser chrome is now drawn by shared composables that read + `Res.string`, and those run in `:napplet` — where the lookup threw + MissingResourceException ("Android context is not initialized") and killed + the sandbox. `multiprocess` is the platform's answer to exactly this: it + gives every process of the app its own instance of the provider. A second + <provider> element cannot work, because the manifest merger keys them by + android:name and would merge the two into one. + --> + <provider + android:name="org.jetbrains.compose.resources.AndroidContextProvider" + android:multiprocess="true" + tools:node="merge" /> + <!-- Sandboxed napplet/nsite host. Runs in an isolated process that holds no keys. --> <activity android:name="com.vitorpamplona.amethyst.napplethost.NappletHostActivity" diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt index da634a5ecf..558c750bb7 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt @@ -268,6 +268,7 @@ class NappletBrowserActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + SandboxComposeResources.ensure(this) // A new window a page opened: its WebView already exists (built inside the opener's onCreateWindow). val popupToken = intent.getStringExtra(EXTRA_POPUP_TOKEN) @@ -1178,8 +1179,15 @@ class NappletBrowserActivity : ComponentActivity() { private fun updateTaskDescription() { val label = pageTitle ?: title.ifBlank { null } ?: BrowserChrome.displayHost(currentUrl()) val color = themeColor ?: 0 + // The Builder's `setIcon` that takes an `Icon` is API 37. We compile + // against 37, so it resolves, and the old guard was `TIRAMISU` — which + // meant every device from 33 to 36 called a method its framework does + // not have and died with NoSuchMethodError the moment a page delivered + // a favicon. Lint's NewApi did not flag it. The deprecated constructor + // takes the same three things and carries the bitmap, so it stays the + // path for everything below 37. val description = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (Build.VERSION.SDK_INT >= ICON_BUILDER_SDK) { ActivityManager.TaskDescription .Builder() .setLabel(label) @@ -1637,6 +1645,9 @@ class NappletBrowserActivity : ComponentActivity() { private const val EXTRA_IS_FAVORITE = "isFavorite" private const val EXTRA_POPUP_TOKEN = "popupToken" + /** `TaskDescription.Builder.setIcon(Icon)` exists from this SDK on. */ + private const val ICON_BUILDER_SDK = 37 + fun intent( context: Context, url: String, diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt index b16ab84463..b8f57894a0 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt @@ -259,6 +259,7 @@ class NappletHostActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + SandboxComposeResources.ensure(this) if (!readManifestExtras()) { Toast.makeText(this, getString(R.string.napplet_invalid), Toast.LENGTH_SHORT).show() diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/SandboxComposeResources.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/SandboxComposeResources.kt new file mode 100644 index 0000000000..f0688d8af6 --- /dev/null +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/SandboxComposeResources.kt @@ -0,0 +1,59 @@ +/* + * 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.napplethost + +import android.content.Context +import com.vitorpamplona.quartz.utils.Log + +/** + * Gives this process the Context that Compose Resources needs. + * + * The browser chrome is drawn by shared composables that read `Res.string`, and + * they run here, in `:napplet`. Compose Resources learns its Context from a + * ContentProvider the library declares, and a provider is only instantiated in + * the process that owns it — so every string lookup in the sandbox died with + * MissingResourceException, taking the window with it. + * + * `android:multiprocess="true"` (set on that provider in the app manifest) lets + * each process hold its own instance, but Android creates it lazily, on first + * access. Nothing in `:napplet` ever addresses the provider by authority, so + * without this it is never created. Acquiring a client once is that first + * access; the provider's `onCreate` then records this process's Context and + * every later lookup resolves locally, with no IPC. + * + * Call before anything composes. It is cheap and idempotent. + */ +object SandboxComposeResources { + private var done = false + + fun ensure(context: Context) { + if (done) return + done = true + val authority = "${context.packageName}.resources.AndroidContextProvider" + runCatching { + context.contentResolver.acquireContentProviderClient(authority)?.close() + }.onFailure { + // Not fatal on its own: the failure surfaces later as a missing + // string, which is easier to read with this line above it. + Log.w("SandboxComposeResources", "could not warm $authority: ${it.message}") + } + } +} From a930fc97f883d764b38682d22da0e20802785251 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona <vitor@vitorpamplona.com> Date: Sat, 26 Sep 2026 18:02:53 -0400 Subject: [PATCH 19/60] design(browser): let the collapsed handle go quiet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The handle is on screen the whole time a page is, and almost nobody pulls it down — it is there for when you are stuck on a site, not as a status display. Tinting it with the Tor accent lit it up on every page, because onion routing is the normal case here rather than the exception, and an accent that is always on is one nobody reads. So it spends colour only on what a reader should act on: plain HTTP keeps the error colour, the console-error dot stays (5dp at 80%, down from a solid 7dp), and everything else — Tor included — is the same muted grey. Tor is not hidden; it keeps its badge inside the pill, where the address row says "Onion-routed" in words for anyone who opens it. I also tried dropping the handle's background, and put it back: on a dark page it nearly vanished, and the point is to call less attention, not to be undiscoverable. `BrowserPillRenderTest` needed a note. Its "did anything render" guard is a flat `distinct colours > 20` over the whole canvas, and `05-handles` — three small bars on mostly empty space — fell to 18 once the accent came off. That guard was in effect asserting the handle has a saturated accent, which is the thing being removed. Rather than lower it for all eleven images, that one gets `minColours = 12`: a blank render scores 1-3, so it still catches the failure the guard exists for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- .../commons/browser/ui/pill/BrowserPill.kt | 20 ++++++++++++------- .../browser/ui/pill/BrowserPillRenderTest.kt | 10 ++++++++-- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPill.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPill.kt index 3c261ee149..2c93718755 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPill.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPill.kt @@ -141,9 +141,16 @@ fun BrowserPill( } /** - * The collapsed grabber. It tells the page's story before it's opened: the bar takes the error colour on - * plain HTTP and the Tor accent when onion-routed, a hairline fills while the page loads, and a dot - * appears when the console has errors. + * The collapsed grabber. + * + * It is on screen the whole time a page is, and almost nobody pulls it down: it is there for when you + * are stuck on a site, not as a status display. So it stays quiet, and spends colour only on the states + * a reader should act on. + * + * Onion routing is not one of them. It is the normal case here rather than an exception, and an accent + * that is always lit is one nobody reads — it just makes the handle loud on every page. Tor keeps its + * badge inside the pill, where the address and "Onion-routed" say it in words for anyone who opens it. + * What is left in the handle is plain HTTP, which is a warning, and a dot for console errors. */ @Composable fun PillHandle( @@ -155,8 +162,7 @@ fun PillHandle( val barColor = when (ui.security) { BrowserChrome.Security.HTTP -> MaterialTheme.colorScheme.error - BrowserChrome.Security.TOR -> MaterialTheme.colorScheme.tertiary - else -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + else -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.45f) } Box( modifier @@ -200,9 +206,9 @@ fun PillHandle( Modifier .align(Alignment.CenterEnd) .padding(start = 44.dp) - .size(7.dp) + .size(5.dp) .clip(CircleShape) - .background(MaterialTheme.colorScheme.error), + .background(MaterialTheme.colorScheme.error.copy(alpha = 0.8f)), ) } } diff --git a/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillRenderTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillRenderTest.kt index 3ac0d504e9..d449a33afb 100644 --- a/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillRenderTest.kt +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillRenderTest.kt @@ -42,6 +42,7 @@ class BrowserPillRenderTest { name: String, widthDp: Int, heightDp: Int, + minColours: Int = 20, content: @Composable () -> Unit, ) { val density = 2f @@ -61,7 +62,7 @@ class BrowserPillRenderTest { val pixels = image.peekPixels() ?: error("no pixels for $name") val distinct = HashSet<Int>() for (y in 0 until height step 7) for (x in 0 until width step 7) distinct += pixels.getColor(x, y) - assertTrue(distinct.size > 20, "$name rendered almost nothing (${distinct.size} colours)") + assertTrue(distinct.size > minColours, "$name rendered almost nothing (${distinct.size} colours)") } finally { scene.close() } @@ -75,7 +76,12 @@ class BrowserPillRenderTest { @Test fun napplet() = render("04-napplet", 820, 800) { BrowserPillNappletPreview() } - @Test fun handles() = render("05-handles", 820, 160) { PillHandlesPreview() } + // Three small bars on a mostly empty canvas, and deliberately the quietest thing + // the redesign draws — so it clears the "did anything render" bar by less than the + // full screens do. It scored 18 when the accent came off; a blank render is 1-3, so + // 12 still catches the failure this guard is for without demanding a colour the + // component is not supposed to have. + @Test fun handles() = render("05-handles", 820, 160, minColours = 12) { PillHandlesPreview() } @Test fun find() = render("06-find", 820, 220) { FindInPagePreview() } From 3d03c82970d3c932c9952d5518b3e4a7bb7b8831 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 22:03:54 +0000 Subject: [PATCH 20/60] fix: audit follow-ups for the commons move - Compose string whitespace, backup labels, store edge cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The one real regression predates this branch: Compose resources draw a value's XML whitespace verbatim, where aapt collapsed it. Values wrapped over indented lines rendered with a leading line break and eight spaces, which turns the account-backup tips' markdown heading into a code block, and Crowdin's stray translator edge spaces (" miejsca zniknęły", Hindi's " दि॰" suffix) showed up. fix_escapes.py now applies aapt's rule to line-break/tab runs and trims an edge space a translation has but its source lacks. The Crowdin workflow already runs it after each sync, and compose_escaping_check.py now fails on raw line breaks. 598 values across 54 files repaired. Also: - rememberPresentation resolves only the labels its diff type writes, instead of all 16 on every recomposition. A test pins the list against presentationOf. - ScheduledPostStore: a failed stat is "no file" (okio posix throws on EACCES where File.exists() did not), symlinked stores still count, no chmod against an injected non-system FileSystem, the cleanup log keeps its throwable. - ClickableEmail percent-encodes '%' in the mailto: URI. - Dropped a misleading WatchScrollToTop import, a stray blank line, and a stale doc path; noted the Base64 pad-bit difference beside the decoder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjQQN9CgWHVtNCSWqnKoqY --- .claude/hooks/compose_escaping_check.py | 14 +++ .../backups/BackupConflictPresentation.kt | 29 +++++-- .../chats/cordnGroup/CordnGroupInfoScreen.kt | 1 - .../notifications/NotificationScreen.kt | 1 - .../backups/BackupPresentationLabelsTest.kt | 60 +++++++++++++ .../2026-08-30-commons-migration-sweep.md | 33 +++++++ .../napplet/protocol/NappletProtocolJson.kt | 2 + .../scheduledposts/ScheduledPostStore.kt | 41 +++++++-- .../commons/util/OwnerOnlyFile.ios.kt | 3 +- .../commons/util/OwnerOnlyFile.jvmAndroid.kt | 2 +- .../values-ar-rSA/strings.xml | 62 ++++---------- .../values-bn-rBD/strings.xml | 37 ++------ .../composeResources/values-cs/strings.xml | 34 ++------ .../values-de-rDE/strings.xml | 36 ++------ .../composeResources/values-de/strings.xml | 44 +++------- .../values-el-rGR/strings.xml | 40 ++------- .../values-eo-rUY/strings.xml | 39 ++------- .../composeResources/values-eo/strings.xml | 42 ++------- .../values-es-rES/strings.xml | 39 ++------- .../values-es-rMX/strings.xml | 37 ++------ .../values-es-rUS/strings.xml | 37 ++------ .../composeResources/values-es/strings.xml | 39 ++------- .../values-fa-rIR/strings.xml | 60 ++++--------- .../composeResources/values-fa/strings.xml | 53 +++--------- .../values-fi-rFI/strings.xml | 36 ++------ .../values-fr-rCA/strings.xml | 31 ++----- .../values-fr-rFR/strings.xml | 31 ++----- .../composeResources/values-fr/strings.xml | 31 ++----- .../values-hi-rIN/strings.xml | 43 +++------- .../values-hu-rHU/strings.xml | 48 +++-------- .../values-in-rID/strings.xml | 46 +++------- .../composeResources/values-in/strings.xml | 42 ++------- .../values-it-rIT/strings.xml | 37 ++------ .../values-ja-rJP/strings.xml | 39 ++------- .../composeResources/values-ja/strings.xml | 42 ++------- .../values-ko-rKR/strings.xml | 37 ++------ .../values-lv-rLV/strings.xml | 37 ++------ .../composeResources/values-nl/strings.xml | 17 +--- .../values-pl-rPL/strings.xml | 38 ++------- .../values-pt-rBR/strings.xml | 40 +++------ .../values-pt-rPT/strings.xml | 40 +++------ .../values-ru-rRU/strings.xml | 39 ++------- .../values-ru-rUA/strings.xml | 37 ++------ .../composeResources/values-ru/strings.xml | 41 ++------- .../values-sl-rSI/strings.xml | 85 ++++++------------- .../values-sr-rSP/strings.xml | 37 ++------ .../values-sv-rSE/strings.xml | 38 ++------- .../composeResources/values-sw/strings.xml | 37 ++------ .../values-ta-rIN/strings.xml | 49 +++-------- .../composeResources/values-ta/strings.xml | 52 +++--------- .../values-th-rTH/strings.xml | 26 ++---- .../composeResources/values-th/strings.xml | 26 ++---- .../values-tr-rTR/strings.xml | 39 ++------- .../composeResources/values-tr/strings.xml | 42 ++------- .../values-uk-rUA/strings.xml | 39 ++------- .../composeResources/values-uk/strings.xml | 42 ++------- .../values-uz-rUZ/strings.xml | 37 ++------ .../values-vi-rVN/strings.xml | 37 ++------ .../values-zh-rCN/strings.xml | 39 ++------- .../values-zh-rHK/strings.xml | 41 ++------- .../values-zh-rSG/strings.xml | 39 ++------- .../values-zh-rTW/strings.xml | 31 ++----- .../composeResources/values-zh/strings.xml | 39 ++------- .../composeResources/values/strings.xml | 40 ++------- .../commons/ui/components/ClickableLinks.kt | 3 +- .../plans/2026-06-21-napplet-desktop-host.md | 2 +- tools/strings-migrate/fix_escapes.py | 57 ++++++++++++- 67 files changed, 699 insertions(+), 1715 deletions(-) create mode 100644 amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupPresentationLabelsTest.kt diff --git a/.claude/hooks/compose_escaping_check.py b/.claude/hooks/compose_escaping_check.py index cd9b054eef..b5721481ae 100755 --- a/.claude/hooks/compose_escaping_check.py +++ b/.claude/hooks/compose_escaping_check.py @@ -27,6 +27,9 @@ Repair with: idempotent, quote-unwrapping is not, and a second unwrap strips the real display quotes from values like `import_follows_tips`. +Raw line breaks and tabs inside a value are the whitespace half of the same +mismatch: aapt collapsed them, Compose renders them (the same repair fixes them). + Only the Compose catalog is scanned. In an Android res tree the same escaping is correct and must be left alone. """ @@ -41,6 +44,10 @@ CATALOG = "*/src/*/composeResources/values*/strings.xml" # Compose resolves those itself. ANDROID_ESCAPE = re.compile(r"(?<!\\)\\(['\"?@])") TOOLS_ATTR = re.compile(r'tools:[\w.-]+="') +# A value whose text holds a raw line break or tab. aapt collapsed that XML layout to +# one space; Compose draws it, and a markdown value indented by it becomes a code block. +STRING_TEXT = re.compile(r"<(string|item)\b[^>]*>(.*?)</\1>", re.S) +LAYOUT_WS = re.compile(r"[\r\n\t]") def find_violations(root: Path): @@ -52,6 +59,13 @@ def find_violations(root: Path): n = len(TOOLS_ATTR.findall(text)) if n: found[path]["tools: attribute"] += n + n = sum( + 1 + for m in STRING_TEXT.finditer(text) + if "<![CDATA[" not in m.group(2) and LAYOUT_WS.search(m.group(2)) + ) + if n: + found[path]["raw line break/tab in a value"] += n return found diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt index ff28dfa6c3..0b4bb8af80 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictPresentation.kt @@ -332,15 +332,23 @@ private fun arrow( */ @Composable fun rememberPresentation(diff: EventDiff): DiffPresentation { - // Compose resources resolve only in composition (or suspended), so the few labels - // [presentationOf] writes into item text are resolved here and handed in. The map - // compares by value, so a recomposition with the same locale keeps the cached result. - val labels = PRESENTATION_LABELS.associateWith { stringRes(it) } + // Compose resources resolve only in composition (or suspended), so the labels + // [presentationOf] writes into item text are resolved here and handed in. Only the + // diff types that write labels get any, so the rest pay for no resource reads. The + // map compares by value: a recomposition with the same locale keeps the cached result. + val labels = labelsWrittenBy(diff).associateWith { stringRes(it) } return remember(diff, labels) { presentationOf(diff) { labels.getValue(it) } } } -/** Every label [presentationOf] resolves through its `str` parameter. */ -private val PRESENTATION_LABELS = +/** Every label [presentationOf] resolves through its `str` parameter for [diff]. */ +internal fun labelsWrittenBy(diff: EventDiff): List<StringResource> = + when (diff) { + is MetadataDiff -> PROFILE_FIELD_LABELS + is AdvertisedRelayListDiff -> RELAY_MARKER_LABELS + else -> emptyList() + } + +private val PROFILE_FIELD_LABELS = listOf( Res.string.backup_profile_field_name, Res.string.backup_profile_field_display_name, @@ -355,12 +363,19 @@ private val PRESENTATION_LABELS = Res.string.backup_profile_field_pronouns, Res.string.backup_profile_field_birthday, Res.string.backup_profile_field_bot, + ) + +private val RELAY_MARKER_LABELS = + listOf( Res.string.backup_conflict_relay_read_write, Res.string.backup_conflict_relay_read_only, Res.string.backup_conflict_relay_write_only, ) -/** Turns each event's own diff into typed, labelled groups. [str] resolves the labels. */ +/** + * Turns each event's own diff into typed, labelled groups. [str] resolves the labels; a + * label it asks for must be in [labelsWrittenBy] for that diff type. + */ fun presentationOf( diff: EventDiff, str: (StringResource) -> String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupInfoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupInfoScreen.kt index b08e44b341..e134442396 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupInfoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupInfoScreen.kt @@ -283,7 +283,6 @@ private fun CordnGroupInfo( * roster, and a rename stayed the old name. The runtime's own methods pair * each commit with that refresh. */ - fun runAdmin( who: String? = null, block: suspend (CordnRuntime) -> Unit, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt index 208c12bb85..cd772a90de 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt @@ -40,7 +40,6 @@ import com.vitorpamplona.amethyst.commons.resources.notification_tab_following import com.vitorpamplona.amethyst.commons.ui.feeds.PagerStateKeys import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.commons.ui.feeds.ScrollStateKeys -import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverLazyListState import com.vitorpamplona.amethyst.commons.ui.feeds.rememberForeverPagerState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupPresentationLabelsTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupPresentationLabelsTest.kt new file mode 100644 index 0000000000..bd28de71ee --- /dev/null +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupPresentationLabelsTest.kt @@ -0,0 +1,60 @@ +/* + * 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.backups + +import com.vitorpamplona.quartz.nip01Core.diff.EventDiff +import com.vitorpamplona.quartz.nip01Core.diff.ListDiff +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataDiff +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListDiff +import org.jetbrains.compose.resources.StringResource +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * rememberPresentation resolves labels in composition and hands presentationOf a lookup + * over exactly [labelsWrittenBy]; a label presentationOf asks for that is not in that list + * crashes the review screen. presentationOf asks for every label of a diff type + * regardless of which fields changed, so an empty diff of each type exercises them all. + */ +class BackupPresentationLabelsTest { + private fun <T> empty() = ListDiff<T>(emptyList(), emptyList(), emptyList()) + + private fun asked(diff: EventDiff): Set<StringResource> { + val asked = mutableSetOf<StringResource>() + presentationOf(diff) { + asked.add(it) + "" + } + return asked + } + + @Test + fun metadataLabelsAreAllProvided() { + val diff = MetadataDiff(null, null, null, null, null, null, null, null, null, null, null, null, null, empty(), empty()) + assertEquals(labelsWrittenBy(diff).toSet(), asked(diff)) + } + + @Test + fun relayListLabelsAreAllProvided() { + val diff = AdvertisedRelayListDiff(empty()) + assertEquals(labelsWrittenBy(diff).toSet(), asked(diff)) + } +} diff --git a/commons/plans/2026-08-30-commons-migration-sweep.md b/commons/plans/2026-08-30-commons-migration-sweep.md index d1a325d81e..46de62e8af 100644 --- a/commons/plans/2026-08-30-commons-migration-sweep.md +++ b/commons/plans/2026-08-30-commons-migration-sweep.md @@ -1399,3 +1399,36 @@ real blocker: differ in primary, secondary, surfaces and outlines, and every shared token (`placeholderText`, `chatBubbleThem`, …) is frozen from the app palette. This is a design decision, not a move. + +### Audit of the 2026-09-26 round + +The four commits were reviewed file by file against the originals. They had no +functional bugs. The follow-ups: + +- **Compose resources keep XML whitespace; Android didn't.** This is older than this + round (it dates back to the first string moves), and it is the one real + regression. aapt collapses every whitespace run in an unquoted value and trims + the ends; Compose draws the XML text verbatim. So the values wrapped over + indented lines (`account_backup_tips2_md`, `push_server_install_app_description`, + `couldnt_find_nwc_wallets_description`, the chat explainers, and 349 translated + values) rendered with a leading line break and eight spaces. In the `_md` strings + CommonMark turns that into a code block. Crowdin also exports a translator's stray + edge space, which aapt used to drop, for example ` miejsca zniknęły` and Hindi's + ` दि॰` time suffix. `fix_escapes.py` now applies aapt's rule to runs holding a line + break or tab. In a translation it also trims an edge space its source string lacks, + which keeps deliberate ones like `" and "`. The Crowdin workflow already runs it + after every sync, and `compose_escaping_check.py` now fails on raw line breaks, so + a sync can't bring it back. 598 values across 54 files were repaired. +- `rememberPresentation` read all 16 backup labels on every recomposition, even for + diff types that write none. It now reads only the ones its diff type uses, and + `BackupPresentationLabelsTest` fails if `presentationOf` asks for a label that list + lacks. +- `ScheduledPostStore`: a failed stat now counts as "no file" rather than throwing + (okio's posix metadata throws on EACCES where `File.exists()` returned false). + Symlinked store files still count as present. The chmod is skipped for an injected + non-system `FileSystem`, and the cleanup log keeps its throwable. +- `ClickableEmail` percent-encodes `%` in the `mailto:` URI (RFC 6068). +- Left as-is, by design: Kotlin's Base64 rejects non-zero pad bits (`"SGl="`) that + Java ignored, which no browser encoder produces. `%1$d` arguments now render ASCII + digits in every locale, the same as every other migrated string. + `ChannelFeedContentState` reaches `LocalCache.appHost` for its main-thread check. diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/protocol/NappletProtocolJson.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/protocol/NappletProtocolJson.kt index af171f6188..c72f1bf825 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/protocol/NappletProtocolJson.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/protocol/NappletProtocolJson.kt @@ -60,6 +60,8 @@ object NappletProtocolJson { private val json = Json { ignoreUnknownKeys = true } // Accepts unpadded input, as java.util.Base64's decoder did before this moved to commonMain. + // One difference remains: non-zero pad bits ("SGl=") are rejected where Java ignored them. + // Browsers' btoa/readAsDataURL never produce those, so only a hand-crafted payload hits it. private val lenientBase64 = Base64.withPadding(Base64.PaddingOption.PRESENT_OPTIONAL) /** The `type` discriminant of a request envelope, used to build the matching `.result` type. */ diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostStore.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostStore.kt index ec34e46725..c3eb84304f 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostStore.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/scheduledposts/ScheduledPostStore.kt @@ -377,7 +377,7 @@ class ScheduledPostStore( // Secure a pre-existing file up front (an older build may have left it at the // 0644 umask default) so it's owner-only even if nothing mutates the store // this session. - if (fileSystem.exists(storageFile)) restrictFileToOwner(storageFile, TAG) + if (storeFileExists()) restrictToOwner(storageFile) var dirty = purgeStale(nowSec()) // Recover claims stranded by a crash-mid-publish so they aren't lost forever. if (recoverStuckClaimsLocked(nowSec())) dirty = true @@ -426,10 +426,12 @@ class ScheduledPostStore( try { storageFile.parent?.let { fileSystem.createDirectories(it) } fileSystem.write(tmp) { writeUtf8(json.encodeToString(ScheduledPostFile(version = 1, posts = snapshot))) } - // Restrict to owner-only BEFORE the rename so the store is never briefly + // Restrict to owner-only BEFORE the rename, so the store file itself is never // world-readable. It holds pre-signed events + the account's pubkey, which - // must not leak to other local users on a shared machine. - restrictFileToOwner(tmp, TAG) + // must not leak to other local users on a shared machine. (The tmp file is + // created at the umask default and narrowed right after its write; the + // directory around it is owner-only on every host that uses this store.) + restrictToOwner(tmp) // Replaces an existing target, including on Windows, where a plain rename // onto an existing file fails. fileSystem.atomicMove(tmp, storageFile) @@ -437,13 +439,38 @@ class ScheduledPostStore( Log.e(TAG, "Failed to persist scheduled posts to $storageFile", e) try { fileSystem.delete(tmp) - } catch (e: Exception) { - Log.w(TAG) { "Failed to clean up temp file $tmp after persist exception: ${e.message}" } + } catch (cleanupError: Exception) { + Log.w(TAG, "Failed to clean up temp file $tmp after persist exception", cleanupError) } } } - private fun hasContent(): Boolean = (fileSystem.metadataOrNull(storageFile)?.size ?: 0L) > 0L + /** + * Permissions are a property of the real disk, so a caller that injects another + * [FileSystem] (an in-memory fake) gets no chmod aimed at a same-named real path. + */ + private fun restrictToOwner(path: Path) { + if (fileSystem === platformFileSystem) restrictFileToOwner(path, TAG) + } + + /** + * The store file's metadata, or null when there is none. A directory is not a store + * file. A symlink counts: okio's metadata does not follow links, so its size is the + * link's, and reading it (which does follow) decides whether there is anything in it. + * A stat that fails (EACCES on iOS, where File.exists() used to answer false) is "no + * file" rather than an exception out of the store. + */ + private fun storeFileMetadata() = + try { + fileSystem.metadataOrNull(storageFile)?.takeIf { it.isRegularFile || it.symlinkTarget != null } + } catch (e: Exception) { + Log.w(TAG, "Could not stat $storageFile", e) + null + } + + private fun storeFileExists(): Boolean = storeFileMetadata() != null + + private fun hasContent(): Boolean = (storeFileMetadata()?.size ?: 0L) > 0L private fun readText(): String = fileSystem.read(storageFile) { readUtf8() } diff --git a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/OwnerOnlyFile.ios.kt b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/OwnerOnlyFile.ios.kt index 42b3c0c516..ebd18c664a 100644 --- a/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/OwnerOnlyFile.ios.kt +++ b/commons/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/util/OwnerOnlyFile.ios.kt @@ -35,6 +35,7 @@ actual fun restrictFileToOwner( tag: String, ) { if (chmod(path.toString(), (S_IRUSR or S_IWUSR).convert()) != 0) { - Log.w(tag) { "Could not restrict permissions on $path: errno $errno" } + val error = errno + Log.w(tag) { "Could not restrict permissions on $path: errno $error" } } } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/OwnerOnlyFile.jvmAndroid.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/OwnerOnlyFile.jvmAndroid.kt index 5d4d16c3b7..cbcfe50038 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/OwnerOnlyFile.jvmAndroid.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/OwnerOnlyFile.jvmAndroid.kt @@ -37,6 +37,6 @@ actual fun restrictFileToOwner( } catch (_: UnsupportedOperationException) { // Windows: no POSIX permissions; the user profile's NTFS ACLs apply instead. } catch (e: Exception) { - Log.w(tag) { "Could not restrict permissions on $path: ${e.message}" } + Log.w(tag, "Could not restrict permissions on $path", e) } } diff --git a/commonsUI/src/commonMain/composeResources/values-ar-rSA/strings.xml b/commonsUI/src/commonMain/composeResources/values-ar-rSA/strings.xml index c93820fa84..c9b3131eff 100644 --- a/commonsUI/src/commonMain/composeResources/values-ar-rSA/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ar-rSA/strings.xml @@ -234,7 +234,7 @@ <string name="copy_user_id">نسخ معرف المستخدم</string> <string name="unblock_user">إلغاء حظر عن مستخدم</string> <string name="user_is_blocked_hidden">هذا المستخدم محظور/مكتوم. منشوراته مخفية.</string> - <string name="npub_hex_username">npub, hex, username </string> + <string name="npub_hex_username">npub, hex, username</string> <string name="clear">نظف</string> <string name="app_logo">شعار التطبيق</string> <string name="nsec_npub_hex_private_key">nsec / npub / hex private key</string> @@ -282,15 +282,8 @@ <string name="mark_all_known_as_read">ضع علامة على جميع الرسائل/الاشعارات المعروفة كمقروء</string> <string name="mark_all_new_as_read">تعيين الجديد كمقروء</string> <string name="mark_all_as_read">تعيين الكل كمقروء</string> - <string name="account_backup_tips2_md"> ## النصائح الرئيسية للنسخ الاحتياطي والسلامة - \n\n- يتم تأمين حسابك بمفتاح سري. المفتاح السري هو سلسلة عشوائية طويلة تبدأ بـ **nsec1**. أي شخص لديه مفتاحك السري يمكنه نشر المحتوى باستخدام هويتك. - \n\n- لا تقم بوضع مفتاحك السري في أي موقع أو برنامج لا تثق به. - \n- مطوري Amethyst لن يطلبوا منك **أبدا** المفتاح السري الخاص بك. - \n- حافظ على نسخة احتياطية آمنة لمفتاحك السري لاسترداد الحساب. نوصي باستخدام برنامج لادارة كلمات المرور. - </string> - <string name="account_backup_tips3_md"> للحصول على أمان إضافي، يمكنك تشفير مفتاحك الخاص بكلمة مرور. يبدأ هذا المفتاح المشفر بـ **ncryptsec1** ولا يمكن استخدامه دون كلمة المرور الخاصة بك. - \n\nإذا فقدت كلمة المرور، لن تتمكن من استرداد مفتاحك الخاص. - </string> + <string name="account_backup_tips2_md">## النصائح الرئيسية للنسخ الاحتياطي والسلامة \n\n- يتم تأمين حسابك بمفتاح سري. المفتاح السري هو سلسلة عشوائية طويلة تبدأ بـ **nsec1**. أي شخص لديه مفتاحك السري يمكنه نشر المحتوى باستخدام هويتك. \n\n- لا تقم بوضع مفتاحك السري في أي موقع أو برنامج لا تثق به. \n- مطوري Amethyst لن يطلبوا منك **أبدا** المفتاح السري الخاص بك. \n- حافظ على نسخة احتياطية آمنة لمفتاحك السري لاسترداد الحساب. نوصي باستخدام برنامج لادارة كلمات المرور.</string> + <string name="account_backup_tips3_md">للحصول على أمان إضافي، يمكنك تشفير مفتاحك الخاص بكلمة مرور. يبدأ هذا المفتاح المشفر بـ **ncryptsec1** ولا يمكن استخدامه دون كلمة المرور الخاصة بك. \n\nإذا فقدت كلمة المرور، لن تتمكن من استرداد مفتاحك الخاص.</string> <string name="encrypt_and_copy_my_secret_key">تشفير و نسخ المفتاح الخاص</string> <string name="badge_award_image_for">صورة شارة مكافئة ل %1$s</string> <string name="badge_award_image">"صورة منح الشارة</string> @@ -311,7 +304,7 @@ <string name="quick_action_request_deletion_gallery_alert_body_v2">إزالة هذه الوسائط من معرضك.</string> <string name="quick_action_request_deletion_alert_title">طلب حذفها</string> <string name="quick_action_request_deletion_alert_body">سيطلب Amethyst حذف ملاحظتك من المرحلات المتصل بها حاليا، لا يوجد ضمان لحذفها من هذه المرحلات أو من المرحلات الأخرى التي ربما حفظت فيها.</string> - <string name="quick_action_block_dialog_btn">حظر </string> + <string name="quick_action_block_dialog_btn">حظر</string> <string name="quick_action_delete_dialog_btn">حذف</string> <string name="quick_action_block">حظر</string> <string name="quick_action_report">التبليغ</string> @@ -693,8 +686,7 @@ <string name="add_caption">إضافة شرح توضيحي</string> <string name="add_caption_example">صديقي المميز</string> <string name="content_description">وصف المحتوى/المحتويات</string> - <string name="content_description_example">A blue boat in a white sandy beach at sunset -(قارب أزرق في شاطئ رملي أبيض عند غروب الشمس)</string> + <string name="content_description_example">A blue boat in a white sandy beach at sunset (قارب أزرق في شاطئ رملي أبيض عند غروب الشمس)</string> <string name="ai_suggested_alt_text_hint">اقتراح من الذكاء الاصطناعي، يمكنك تعديله</string> <string name="ai_suggested_alt_text_dismiss">رفض اقتراح الذكاء الاصطناعي</string> <string name="zap_type">نوع الزاب</string> @@ -847,7 +839,7 @@ <string name="relay_filter_since">منذ %1$s</string> <string name="relay_filter_until">حتى %1$s</string> <string name="relay_filter_limit">الحد %1$d</string> - <string name="minimum_prefix">الحد الأدنى لل prefix </string> + <string name="minimum_prefix">الحد الأدنى لل prefix</string> <string name="maximum_event_tags">الحد الأقصى لوسوم الحدث</string> <string name="max_content_length">الحد الأقصى لطول المحتوى</string> <string name="discards_older_than">تُهمَل أقدم من</string> @@ -886,10 +878,8 @@ <string name="followed_tags">العلامات المُتابعة</string> <string name="discover_follows_explainer">هذه قوائم بالمستخدمين التي توصي بها للآخرين. يُسمح فقط بالمستخدمين العامين.</string> <string name="community_approved_posts">المنشورات الموافق عليها</string> - <string name="groups_no_descriptor">هذه المجموعة ليس لديها وصف أو قواعد. - تحدث إلى المالك لإضافة مستخدم جديد</string> - <string name="community_no_descriptor">هذا المجتمع ليس لديه وصف أو قواعد. - تحدث إلى المالك لإضافة مستخدم جديد</string> + <string name="groups_no_descriptor">هذه المجموعة ليس لديها وصف أو قواعد. تحدث إلى المالك لإضافة مستخدم جديد</string> + <string name="community_no_descriptor">هذا المجتمع ليس لديه وصف أو قواعد. تحدث إلى المالك لإضافة مستخدم جديد</string> <string name="user_preferences">تفضيلات المستخدم</string> <string name="settings_search_placeholder">البحث في الإعدادات</string> <string name="settings_search_no_results">لا توجد إعدادات لـ "%1$s"</string> @@ -982,9 +972,7 @@ <string name="push_server_none_explainer">تعطيل الإشعارات</string> <string name="push_server_uses_app_explainer">يتم استخدام هذا التطبيق: %1$s</string> <string name="push_server_install_app">إعدادات الإشعارات</string> - <string name="push_server_install_app_description">لاستخدام الإشعارات حمل أي تطبيق يدعم ال [Unified Push](https://unifiedpush.org/) -مثل [Ntfy](https://ntfy.sh/). -بعد تثبيتك التطبيق قم باختياره من الإعدادات</string> + <string name="push_server_install_app_description">لاستخدام الإشعارات حمل أي تطبيق يدعم ال [Unified Push](https://unifiedpush.org/) مثل [Ntfy](https://ntfy.sh/). بعد تثبيتك التطبيق قم باختياره من الإعدادات</string> <string name="thread_title">الخيط</string> <string name="send_the_seller_a_message">أرسل رسالة إلى البائع</string> <string name="hi_there_is_this_still_available">مرحبًا، هل هذا لا يزال متاحًا؟</string> @@ -1876,7 +1864,7 @@ <string name="biometric_authentication_failed">فشلت المصادقة</string> <string name="biometric_authentication_failed_explainer">فشل التحقق البيومتري في مصادقة مالك هذا الهاتف</string> <string name="biometric_authentication_failed_explainer_with_error">فشل التحقق البيومتري في مصادقة مالك هذا الهاتف. الخطأ: %1$s</string> - <string name="block_only">حظر </string> + <string name="block_only">حظر</string> <string name="blocked_section_explainer">لن يتصل Amethyst أبدًا بهذه الـ relays</string> <string name="blocked_users">المستخدمين المحظورين</string> <string name="bookmark_list_creation_screen_title">قائمة إشارات مرجعية جديدة</string> @@ -1986,8 +1974,7 @@ <string name="cashu_settings_delete_confirm_body">نشر طلب حذف لتوصية kind:38000 الخاصة بك لـ %1$s. ستحذفها الـ relays التي تحترم NIP-09.</string> <string name="cashu_settings_restore_result">تم استرداد %1$s sats عبر %2$s أدلة.</string> <string name="cashu_successful_redemption">تم استلام Cashu</string> - <string name="cashu_successful_redemption_explainer">%1$s sats أرسلت الى محفظتك -(الرسوم: sats %2$s ) </string> + <string name="cashu_successful_redemption_explainer">%1$s sats أرسلت الى محفظتك (الرسوم: sats %2$s )</string> <plurals name="cashu_untrusted_mint_title"> <item quantity="zero">لا أموال محتجزة في mint غير مستخدم</item> <item quantity="one">أموال محتجزة في %1$d mint لا تستخدمه</item> @@ -2060,15 +2047,7 @@ <string name="could_not_fetch_invoice_from_details">تعذّر جلب الفاتورة من %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">تعذّر فتح الملف المضغوط</string> <string name="couldnt_find_nwc_wallets">لم يتم العثور على المحفظة</string> - <string name="couldnt_find_nwc_wallets_description">لم يتمكن Amethyst من العثور على أي تطبيقات محافظ تدعم Nostr Wallet Connect (NWC). -\n\nما يمكنك فعله: -\n\t1. التحقق من دعم NWC: تأكد من أن تطبيق محفظتك المفضل متوافق مع Nostr Wallet Connect. -\n\t2. إنشاء اتصال: انتقل إلى تطبيق محفظتك وأنشئ اتصال NWC جديدًا. -\n\t3. الربط بـ Amethyst: عادةً ما تتيح محفظتك ثلاثة خيارات: -\n\t\t3.1 ربط تلقائي: زر لفتح Amethyst والاتصال به تلقائيًا. إذا كان الأمر كذلك، اضغط على الزر، وافتح Amethyst واضغط على حفظ في الشاشة الجديدة. -\n\t\t3.2 ربط يدوي: عنوان URI للاتصال يمكنك نسخه والضغط على أيقونة اللصق في هذه الشاشة لإدخاله مباشرةً. -\n\t\t3.3 رمز QR: تنشئ المحفظة رمز QR يمكنك قراءته من زر رمز QR في هذه الشاشة. - </string> + <string name="couldnt_find_nwc_wallets_description">لم يتمكن Amethyst من العثور على أي تطبيقات محافظ تدعم Nostr Wallet Connect (NWC). \n\nما يمكنك فعله: \n\t1. التحقق من دعم NWC: تأكد من أن تطبيق محفظتك المفضل متوافق مع Nostr Wallet Connect. \n\t2. إنشاء اتصال: انتقل إلى تطبيق محفظتك وأنشئ اتصال NWC جديدًا. \n\t3. الربط بـ Amethyst: عادةً ما تتيح محفظتك ثلاثة خيارات: \n\t\t3.1 ربط تلقائي: زر لفتح Amethyst والاتصال به تلقائيًا. إذا كان الأمر كذلك، اضغط على الزر، وافتح Amethyst واضغط على حفظ في الشاشة الجديدة. \n\t\t3.2 ربط يدوي: عنوان URI للاتصال يمكنك نسخه والضغط على أيقونة اللصق في هذه الشاشة لإدخاله مباشرةً. \n\t\t3.3 رمز QR: تنشئ المحفظة رمز QR يمكنك قراءته من زر رمز QR في هذه الشاشة.</string> <string name="create">إنشاء</string> <string name="d">يوم</string> <string name="danger_zone">منطقة الخطر</string> @@ -2451,7 +2430,7 @@ <string name="login_with_a_private_key_to_be_able_to_send_zaps">قم بتسجيل الدخول باستخدام مفتاح خاص لتتمكن من إرسال Zaps</string> <string name="login_with_a_private_key_to_be_able_to_show_word">قم بتسجيل الدخول باستخدام مفتاحك السري لتتمكن من إظهار الكلمة أو الجملة</string> <string name="login_with_a_private_key_to_be_able_to_sign_events">أنت تستخدم مفتاحًا عامًا وهو للقراءة فقط. سجّل الدخول بمفتاح خاص لتتمكن من توقيع الأحداث</string> - <string name="login_with_a_private_key_to_be_able_to_unfollow">قم بتسجيل الدخول بمفتاحك الخاص للتتمكن من إلغاء متابعة المستخدمين </string> + <string name="login_with_a_private_key_to_be_able_to_unfollow">قم بتسجيل الدخول بمفتاحك الخاص للتتمكن من إلغاء متابعة المستخدمين</string> <string name="login_with_a_private_key_to_be_able_to_upload">أنت تستخدم مفتاحًا عامًا وهو للقراءة فقط. سجّل الدخول بمفتاح خاص لتتمكن من الرفع</string> <string name="login_with_a_private_key_to_like_posts">تسجيل الدخول باستخدام مفتاح خاص لإبداء الإعجاب بالمنشورات</string> <string name="longs">مقاطع فيديو</string> @@ -2786,8 +2765,7 @@ <string name="preview_card_image_for">معاينة صورة البطاقة لـ %1$s</string> <string name="privacy_options">خيارات الخصوصية</string> <string name="privacy_policy">سياسة الخصوصية</string> - <string name="private_conversation_notification"><استحالة فك تشفير الرسالة الخاصة> -\n\n أنت الآن في محادثة خاصة/مشفرة بين %1$s و %2$s .</string> + <string name="private_conversation_notification"><استحالة فك تشفير الرسالة الخاصة> \n\n أنت الآن في محادثة خاصة/مشفرة بين %1$s و %2$s .</string> <string name="private_inbox_section_explainer">أدخل ما بين 1 و3 relays لتكون صندوقًا واردًا خاصًا. سيستخدم الآخرون هذه الـ relays لإرسال رسائل مباشرة إليك. يجب أن تقبل DM Inbox relays أي رسالة من أي شخص، لكن تسمح لك وحدك بتنزيلها. خيارات جيدة:\n - inbox.nostr.wine (مدفوع)\n - auth.nostr1.com (مجاني)\n - you.nostr1.com (relays شخصية - مدفوعة)</string> <string name="private_inbox_section_explainer_profile">المستخدم يستقبل الرسائل المباشرة على هذه الـ relays</string> <string name="private_message">رسالة خاصة</string> @@ -2807,11 +2785,8 @@ <string name="propose_an_edit">اقتراح تعديل</string> <string name="proxy_section_explainer">الـ relays التجميعية التي يجب أن يستخدمها التطبيق لتنزيل تغذياتك منها، مثل filter.nostr.wine. هذا يستبدل نموذج outbox وسيجعل التطبيق يتصل فقط بالـ relays الموجودة في قوائمك.</string> <string name="public_chat">الدردشة العامة</string> - <string name="public_chat_explainer">المحادثات العامة مرئية للجميع على Nostr ويمكن لأي شخص - المشاركة فيها. إنها مناسبة للمجتمعات المفتوحة حول مواضيع محددة. - يمكن التحكم في الإشراف عبر حذف المنشورات على relay المجموعة</string> - <string name="public_chat_relays_explainer">أدخل من 1 إلى 3 relay تستضيف هذه المجموعة. - تستخدم تطبيقات Nostr هذا الإعداد لمعرفة مكان تنزيل الرسائل وإرسالها.</string> + <string name="public_chat_explainer">المحادثات العامة مرئية للجميع على Nostr ويمكن لأي شخص المشاركة فيها. إنها مناسبة للمجتمعات المفتوحة حول مواضيع محددة. يمكن التحكم في الإشراف عبر حذف المنشورات على relay المجموعة</string> + <string name="public_chat_relays_explainer">أدخل من 1 إلى 3 relay تستضيف هذه المجموعة. تستخدم تطبيقات Nostr هذا الإعداد لمعرفة مكان تنزيل الرسائل وإرسالها.</string> <string name="public_chat_title">بيانات المحادثة العامة</string> <string name="public_chats">المحادثات العامة</string> <string name="public_home_section_explainer">يخزّن هذا النوع من الـ relay جميع محتواك. سيرسل Amethyst منشوراتك إليها وسيستخدم الآخرون هذه الـ relays للعثور على محتواك. أدخل ما بين 1 و3 relays. يمكن أن تكون relays شخصية أو مدفوعة أو عامة.</string> @@ -2833,10 +2808,7 @@ <string name="read_only_user">مستخدم في وضع القراءة فقط</string> <string name="record_a_message_description">اضغط مطوّلًا لتسجيل رسالة</string> <string name="relay_chat">محادثة relay</string> - <string name="relay_chat_explainer">محادثات relay هي مجموعات دردشة يتحكم فيها الـ relay المضيف. - إنها مرئية للجميع على Nostr ويمكن لأي شخص المشاركة فيها. - إنها مناسبة للمجتمعات المفتوحة حول مواضيع محددة. بعض هذه المجموعات مؤقتة - وبالتالي تختفي رسائل الدردشة بمرور الوقت</string> + <string name="relay_chat_explainer">محادثات relay هي مجموعات دردشة يتحكم فيها الـ relay المضيف. إنها مرئية للجميع على Nostr ويمكن لأي شخص المشاركة فيها. إنها مناسبة للمجتمعات المفتوحة حول مواضيع محددة. بعض هذه المجموعات مؤقتة وبالتالي تختفي رسائل الدردشة بمرور الوقت</string> <string name="relay_chat_title">محادثات relay</string> <string name="relay_management_tab_events">الأحداث</string> <string name="relay_management_tab_ips">عناوين IP</string> diff --git a/commonsUI/src/commonMain/composeResources/values-bn-rBD/strings.xml b/commonsUI/src/commonMain/composeResources/values-bn-rBD/strings.xml index e575eee7eb..75664b26ee 100644 --- a/commonsUI/src/commonMain/composeResources/values-bn-rBD/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-bn-rBD/strings.xml @@ -271,15 +271,8 @@ <string name="mark_all_known_as_read">পরিচিত সবগুলোকে পঠিত হিসেবে চিহ্নিত করুন</string> <string name="mark_all_new_as_read">নতুন সবগুলোকে পঠিত হিসেবে চিহ্নিত করুন</string> <string name="mark_all_as_read">সবগুলোকে পঠিত হিসেবে চিহ্নিত করুন</string> - <string name="account_backup_tips2_md"> ## মূল ব্যাকআপ এবং নিরাপত্তা টিপস - \n\n আপনার অ্যাকাউন্ট একটি গোপন কী দ্বারা সুরক্ষিত। মূলটি হল অক্ষরগুলির একটি দীর্ঘ ক্রম যা শুরু হয় **nsec1**. যে কেউ এই গোপন কী অ্যাক্সেস করতে পারে পোস্ট এবং আপনার পরিচয় পরিবর্তন করতে পারেন. - \n\n- আপনি বিশ্বাস করেন না এমন কোনো ওয়েবসাইট বা সফ্টওয়্যারে আপনার গোপন কী **করবেন না**. - \n- অ্যামেথিস্ট ডেভেলপাররা **কখনই** আপনার গোপন কী চাইবে না. - \n- **করুন** অ্যাকাউন্ট পুনরুদ্ধারের জন্য আপনার গোপন কীটির একটি নিরাপদ ব্যাকআপ রাখুন৷. আমরা একটি পাসওয়ার্ড ম্যানেজার ব্যবহার করার পরামর্শ দিই।. - </string> - <string name="account_backup_tips3_md"> অতিরিক্ত নিরাপত্তার জন্য, আপনি একটি পাসওয়ার্ড দিয়ে আপনার কী এনক্রিপ্ট করতে পারেন. এই কী দিয়ে শুরু হয় **ncryptsec1** এবং আপনার পাসওয়ার্ড ছাড়া ব্যবহার করা যাবে না।. - \n\n আপনি যদি আপনার পাসওয়ার্ড হারিয়ে ফেলেন, আপনি আপনার কী পুনরুদ্ধার করতে পারবেন না।. - </string> + <string name="account_backup_tips2_md">## মূল ব্যাকআপ এবং নিরাপত্তা টিপস \n\n আপনার অ্যাকাউন্ট একটি গোপন কী দ্বারা সুরক্ষিত। মূলটি হল অক্ষরগুলির একটি দীর্ঘ ক্রম যা শুরু হয় **nsec1**. যে কেউ এই গোপন কী অ্যাক্সেস করতে পারে পোস্ট এবং আপনার পরিচয় পরিবর্তন করতে পারেন. \n\n- আপনি বিশ্বাস করেন না এমন কোনো ওয়েবসাইট বা সফ্টওয়্যারে আপনার গোপন কী **করবেন না**. \n- অ্যামেথিস্ট ডেভেলপাররা **কখনই** আপনার গোপন কী চাইবে না. \n- **করুন** অ্যাকাউন্ট পুনরুদ্ধারের জন্য আপনার গোপন কীটির একটি নিরাপদ ব্যাকআপ রাখুন৷. আমরা একটি পাসওয়ার্ড ম্যানেজার ব্যবহার করার পরামর্শ দিই।.</string> + <string name="account_backup_tips3_md">অতিরিক্ত নিরাপত্তার জন্য, আপনি একটি পাসওয়ার্ড দিয়ে আপনার কী এনক্রিপ্ট করতে পারেন. এই কী দিয়ে শুরু হয় **ncryptsec1** এবং আপনার পাসওয়ার্ড ছাড়া ব্যবহার করা যাবে না।. \n\n আপনি যদি আপনার পাসওয়ার্ড হারিয়ে ফেলেন, আপনি আপনার কী পুনরুদ্ধার করতে পারবেন না।.</string> <string name="encrypt_and_copy_my_secret_key">এনক্রিপ্ট এবং আমার গোপন কী অনুলিপি</string> <string name="badge_award_image_for">%1$s -এর জন্য ব্যাজ পুরষ্কারের ছবি</string> <string name="badge_award_image">"ব্যাজ পুরস্কার চিত্র</string> @@ -963,9 +956,7 @@ <string name="push_server_none_explainer">Push বিজ্ঞপ্তি নিষ্ক্রিয় করে</string> <string name="push_server_uses_app_explainer">অ্যাপ %1$s ব্যবহার করে</string> <string name="push_server_install_app">Push বিজ্ঞপ্তি সেটআপ</string> - <string name="push_server_install_app_description"> পুশ নোটিফিকেশন পেতে, [Unified Push](https://unifiedpush.org/) সমর্থন করে এমন যেকোনো অ্যাপ ইনস্টল করুন, যেমন [Ntfy](https://ntfy.sh/)। - ইনস্টল করার পরে, সেটিংসে আপনি যে অ্যাপটি ব্যবহার করতে চান তা নির্বাচন করুন। - </string> + <string name="push_server_install_app_description">পুশ নোটিফিকেশন পেতে, [Unified Push](https://unifiedpush.org/) সমর্থন করে এমন যেকোনো অ্যাপ ইনস্টল করুন, যেমন [Ntfy](https://ntfy.sh/)। ইনস্টল করার পরে, সেটিংসে আপনি যে অ্যাপটি ব্যবহার করতে চান তা নির্বাচন করুন।</string> <string name="thread_title">থ্রেড</string> <string name="send_the_seller_a_message">বিক্রেতাকে একটি বার্তা পাঠান</string> <string name="hi_there_is_this_still_available">হাই, এটা কি এখনও পাওয়া যায়?</string> @@ -2022,15 +2013,7 @@ <string name="could_not_fetch_invoice_from_details">%1$s থেকে ইনভয়েস আনতে পারেনি: %2$s</string> <string name="could_not_open_the_compressed_file">সংকুচিত ফাইলটি খোলা যায়নি</string> <string name="couldnt_find_nwc_wallets">ওয়ালেট পাওয়া যায়নি</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst কোনো ওয়ালেট অ্যাপ খুঁজে পায়নি যা Nostr Wallet Connect (NWC) সমর্থন করে। -\n\nআপনি যা করতে পারেন: -\n\t১. NWC সমর্থন যাচাই করুন: নিশ্চিত করুন যে আপনার পছন্দের ওয়ালেট অ্যাপ Nostr Wallet Connect-এর সাথে সামঞ্জস্যপূর্ণ। -\n\t২. সংযোগ তৈরি করুন: আপনার ওয়ালেট অ্যাপে যান এবং একটি নতুন NWC সংযোগ তৈরি করুন। -\n\t৩. Amethyst-এ লিঙ্ক করুন: আপনার ওয়ালেট সাধারণত তিনটি বিকল্প দেবে: -\n\t\t৩.১ স্বয়ংক্রিয় লিঙ্ক: একটি বোতাম যা স্বয়ংক্রিয়ভাবে Amethyst খুলে সংযুক্ত করবে। যদি তাই হয়, সেই বোতামে ক্লিক করুন, Amethyst খুলুন এবং নতুন স্ক্রিনে সেভ চাপুন। -\n\t\t৩.২ ম্যানুয়াল লিঙ্ক: একটি Connection URI যা কপি করে এই স্ক্রিনের পেস্ট আইকনে ক্লিক করে সরাসরি ইনপুট করতে পারবেন। -\n\t\t৩.৩ QR কোড: ওয়ালেট একটি QR কোড তৈরি করে যা আপনি এই স্ক্রিনের QR কোড বোতাম থেকে পড়তে পারবেন। - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst কোনো ওয়ালেট অ্যাপ খুঁজে পায়নি যা Nostr Wallet Connect (NWC) সমর্থন করে। \n\nআপনি যা করতে পারেন: \n\t১. NWC সমর্থন যাচাই করুন: নিশ্চিত করুন যে আপনার পছন্দের ওয়ালেট অ্যাপ Nostr Wallet Connect-এর সাথে সামঞ্জস্যপূর্ণ। \n\t২. সংযোগ তৈরি করুন: আপনার ওয়ালেট অ্যাপে যান এবং একটি নতুন NWC সংযোগ তৈরি করুন। \n\t৩. Amethyst-এ লিঙ্ক করুন: আপনার ওয়ালেট সাধারণত তিনটি বিকল্প দেবে: \n\t\t৩.১ স্বয়ংক্রিয় লিঙ্ক: একটি বোতাম যা স্বয়ংক্রিয়ভাবে Amethyst খুলে সংযুক্ত করবে। যদি তাই হয়, সেই বোতামে ক্লিক করুন, Amethyst খুলুন এবং নতুন স্ক্রিনে সেভ চাপুন। \n\t\t৩.২ ম্যানুয়াল লিঙ্ক: একটি Connection URI যা কপি করে এই স্ক্রিনের পেস্ট আইকনে ক্লিক করে সরাসরি ইনপুট করতে পারবেন। \n\t\t৩.৩ QR কোড: ওয়ালেট একটি QR কোড তৈরি করে যা আপনি এই স্ক্রিনের QR কোড বোতাম থেকে পড়তে পারবেন।</string> <string name="create">তৈরি করুন</string> <string name="d">d</string> <string name="danger_zone">বিপদজনক এলাকা</string> @@ -2699,11 +2682,8 @@ <string name="proxy_section">প্রক্সি Relay</string> <string name="proxy_section_explainer">অ্যাগ্রিগেটর relay যা অ্যাপটিকে আপনার ফিড ডাউনলোড করতে অবশ্যই ব্যবহার করতে হবে, যেমন filter.nostr.wine। এটি আউটবক্স মডেলকে প্রতিস্থাপন করে এবং অ্যাপটিকে শুধুমাত্র আপনার তালিকার relay গুলোতে সংযুক্ত করবে।</string> <string name="public_chat">সার্বজনীন আলাপ</string> - <string name="public_chat_explainer">পাবলিক চ্যাটগুলো Nostr-এর সবার কাছে দৃশ্যমান এবং যে কেউ - এতে অংশগ্রহণ করতে পারে। নির্দিষ্ট বিষয়ের চারপাশে উন্মুক্ত সম্প্রদায়ের জন্য এগুলো দুর্দান্ত। - এর relay-তে পোস্ট মুছে পরিচালনা নিয়ন্ত্রণ করা যায়</string> - <string name="public_chat_relays_explainer">এই গ্রুপটি হোস্ট করে এমন ১-৩ টি relay যোগ করুন। - Nostr ক্লায়েন্টগুলো এই সেটিং ব্যবহার করে জানে যে কোথা থেকে বার্তা ডাউনলোড করতে হবে এবং আপনার বার্তা কোথায় পাঠাতে হবে।</string> + <string name="public_chat_explainer">পাবলিক চ্যাটগুলো Nostr-এর সবার কাছে দৃশ্যমান এবং যে কেউ এতে অংশগ্রহণ করতে পারে। নির্দিষ্ট বিষয়ের চারপাশে উন্মুক্ত সম্প্রদায়ের জন্য এগুলো দুর্দান্ত। এর relay-তে পোস্ট মুছে পরিচালনা নিয়ন্ত্রণ করা যায়</string> + <string name="public_chat_relays_explainer">এই গ্রুপটি হোস্ট করে এমন ১-৩ টি relay যোগ করুন। Nostr ক্লায়েন্টগুলো এই সেটিং ব্যবহার করে জানে যে কোথা থেকে বার্তা ডাউনলোড করতে হবে এবং আপনার বার্তা কোথায় পাঠাতে হবে।</string> <string name="public_chat_relays_title">Relay-গুলি</string> <string name="public_chat_title">পাবলিক চ্যাট মেটাডেটা</string> <string name="public_chats">পাবলিক চ্যাট</string> @@ -2728,10 +2708,7 @@ <string name="read_only_user">শুধু-পড়ার ব্যবহারকারী</string> <string name="record_a_message_description">একটি বার্তা রেকর্ড করতে ক্লিক করুন এবং ধরে রাখুন</string> <string name="relay_chat">Relay চ্যাট</string> - <string name="relay_chat_explainer">Relay চ্যাটগুলো হলো চ্যাট গ্রুপ যা তাদের হোম relay দ্বারা নিয়ন্ত্রিত হয়। - এগুলো Nostr-এর সবার কাছে দৃশ্যমান এবং যে কেউ এতে অংশগ্রহণ করতে পারে। - নির্দিষ্ট বিষয়ের চারপাশে উন্মুক্ত সম্প্রদায়ের জন্য এগুলো দুর্দান্ত। এই গ্রুপগুলোর কিছু ক্ষণস্থায়ী - এবং সেজন্য চ্যাট বার্তাগুলো সময়ের সাথে মুছে যায়</string> + <string name="relay_chat_explainer">Relay চ্যাটগুলো হলো চ্যাট গ্রুপ যা তাদের হোম relay দ্বারা নিয়ন্ত্রিত হয়। এগুলো Nostr-এর সবার কাছে দৃশ্যমান এবং যে কেউ এতে অংশগ্রহণ করতে পারে। নির্দিষ্ট বিষয়ের চারপাশে উন্মুক্ত সম্প্রদায়ের জন্য এগুলো দুর্দান্ত। এই গ্রুপগুলোর কিছু ক্ষণস্থায়ী এবং সেজন্য চ্যাট বার্তাগুলো সময়ের সাথে মুছে যায়</string> <string name="relay_chat_title">Relay চ্যাট</string> <string name="relay_management_tab_events">ইভেন্ট</string> <string name="relay_management_tab_ips">আইপি</string> diff --git a/commonsUI/src/commonMain/composeResources/values-cs/strings.xml b/commonsUI/src/commonMain/composeResources/values-cs/strings.xml index 4611482789..b21d6ee05a 100644 --- a/commonsUI/src/commonMain/composeResources/values-cs/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-cs/strings.xml @@ -473,13 +473,8 @@ <string name="mark_all_known_as_read">Označit všechny známé jako přečtené</string> <string name="mark_all_new_as_read">Označit všechny nové jako přečtené</string> <string name="mark_all_as_read">Označit všechny jako přečtené</string> - <string name="account_backup_tips2_md"> ## Tipy pro zálohování klíčů a bezpečnost - \n\nVaše konto je zabezpečeno tajným klíčem. Klíč je dlouhá posloupnost znaků začínající s **nsec1**. Kdo má přístup k tomuto tajnému klíči, může přispívat a měnit vaši identitu. - \n\n- **Nedávejte** svůj tajný klíč na žádnou webovou stránku nebo do žádného softwaru, kterému nedůvěřujete. - \n- Vývojáři Amethystu nikdy **nebudou** žádat o váš tajný klíč. - \n- **Udržujte** bezpečnou zálohu vašeho tajného klíče pro obnovení účtu. Doporučujeme použití správce hesel.</string> - <string name="account_backup_tips3_md"> Pro další zabezpečení můžete svůj klíč zašifrovat heslem. Tento klíč začíná s **ncryptsec1** a nelze ho použít bez vašeho hesla. - \n\nPokud zapomenete heslo, nebudete moci obnovit svůj klíč.</string> + <string name="account_backup_tips2_md">## Tipy pro zálohování klíčů a bezpečnost \n\nVaše konto je zabezpečeno tajným klíčem. Klíč je dlouhá posloupnost znaků začínající s **nsec1**. Kdo má přístup k tomuto tajnému klíči, může přispívat a měnit vaši identitu. \n\n- **Nedávejte** svůj tajný klíč na žádnou webovou stránku nebo do žádného softwaru, kterému nedůvěřujete. \n- Vývojáři Amethystu nikdy **nebudou** žádat o váš tajný klíč. \n- **Udržujte** bezpečnou zálohu vašeho tajného klíče pro obnovení účtu. Doporučujeme použití správce hesel.</string> + <string name="account_backup_tips3_md">Pro další zabezpečení můžete svůj klíč zašifrovat heslem. Tento klíč začíná s **ncryptsec1** a nelze ho použít bez vašeho hesla. \n\nPokud zapomenete heslo, nebudete moci obnovit svůj klíč.</string> <string name="backup_keys_nudge_title">Zálohujte si klíče</string> <string name="backup_keys_nudge_body">Váš tajný klíč je jediný způsob, jak se k tomuto účtu dostat. Pokud ho ztratíte, nelze ho už nikdy obnovit. Uložte si ho někam do bezpečí.</string> <string name="backup_keys_nudge_backup_now">Zálohovat nyní</string> @@ -1598,9 +1593,7 @@ <string name="push_server_none_explainer">Zakázat Push oznámení</string> <string name="push_server_uses_app_explainer">Použít aplikaci %1$s</string> <string name="push_server_install_app">Nastavení Push-oznámení</string> - <string name="push_server_install_app_description"> Pro příjem Push-oznámení nainstalujte libovolnou aplikaci, která podporuje [Unified Push](https://unifiedpush.org/), jako je [Ntfy](https://ntfy.sh/). - Po instalaci vyberte aplikaci kterou chcete používat v Nastavení. - </string> + <string name="push_server_install_app_description">Pro příjem Push-oznámení nainstalujte libovolnou aplikaci, která podporuje [Unified Push](https://unifiedpush.org/), jako je [Ntfy](https://ntfy.sh/). Po instalaci vyberte aplikaci kterou chcete používat v Nastavení.</string> <string name="thread_title">Vlákno</string> <string name="send_the_seller_a_message">Poslat prodejci zprávu</string> <string name="hi_there_is_this_still_available">Ahoj, je to stále k dispozici?</string> @@ -3393,14 +3386,7 @@ <string name="could_not_fetch_invoice_from_details">Nelze získat fakturu z %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Nepodařilo se otevřít komprimovaný soubor</string> <string name="couldnt_find_nwc_wallets">Peněženka nebyla nalezená</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst nenašel žádné aplikace peněženek, které podporují Nostr Wallet Connect (NWC). -\n\nCo můžete udělat: -\n\t1. Ověřte podporu NWC: Zkontrolujte, že vaše oblíbená peněženka podporuje Nostr Wallet Connect. -\n\t2. Vytvořte připojení: Otevřete svou peněženku a vytvořte nové NWC připojení. -\n\t3. Propojte s Amethyst: Vaše peněženka obvykle nabídne tři možnosti: -\n\t\t3.1 Automatické propojení: Tlačítko pro automatické otevření a připojení k Amethyst. Klikněte na něj, otevřete Amethyst a uložte na nové obrazovce. -\n\t\t3.2 Ruční propojení: URI připojení, které můžete zkopírovat a vložit přes ikonu vložení na této obrazovce. -\n\t\t3.3 QR kód: Peněženka vytvoří QR kód, který můžete naskenovat pomocí tlačítka QR kódu na této obrazovce.</string> + <string name="couldnt_find_nwc_wallets_description">Amethyst nenašel žádné aplikace peněženek, které podporují Nostr Wallet Connect (NWC). \n\nCo můžete udělat: \n\t1. Ověřte podporu NWC: Zkontrolujte, že vaše oblíbená peněženka podporuje Nostr Wallet Connect. \n\t2. Vytvořte připojení: Otevřete svou peněženku a vytvořte nové NWC připojení. \n\t3. Propojte s Amethyst: Vaše peněženka obvykle nabídne tři možnosti: \n\t\t3.1 Automatické propojení: Tlačítko pro automatické otevření a připojení k Amethyst. Klikněte na něj, otevřete Amethyst a uložte na nové obrazovce. \n\t\t3.2 Ruční propojení: URI připojení, které můžete zkopírovat a vložit přes ikonu vložení na této obrazovce. \n\t\t3.3 QR kód: Peněženka vytvoří QR kód, který můžete naskenovat pomocí tlačítka QR kódu na této obrazovce.</string> <string name="create">Vytvořit</string> <string name="d">d</string> <string name="danger_zone">Nebezpečná zóna</string> @@ -4611,11 +4597,8 @@ <string name="proxy_section">Proxy přenašeče</string> <string name="proxy_section_explainer">Agregační přenašeče, které aplikace používá ke stahování vašich zdrojů, jako např. filter.nostr.wine</string> <string name="public_chat">Veřejný chat</string> - <string name="public_chat_explainer">Veřejné konverzace jsou viditelné pro všechny na Nostru a kdokoli - se na nich může podílet. Jsou skvělé pro otevřené komunity kolem konkrétních témat. - Moderaci lze ovládat smazáním příspěvků na relé</string> - <string name="public_chat_relays_explainer">Vložte mezi 1–3 relé, které hostí tuto skupinu. - Klienti Nostr používají toto nastavení, aby věděli, kam stahovat zprávy a odesílat zprávy.</string> + <string name="public_chat_explainer">Veřejné konverzace jsou viditelné pro všechny na Nostru a kdokoli se na nich může podílet. Jsou skvělé pro otevřené komunity kolem konkrétních témat. Moderaci lze ovládat smazáním příspěvků na relé</string> + <string name="public_chat_relays_explainer">Vložte mezi 1–3 relé, které hostí tuto skupinu. Klienti Nostr používají toto nastavení, aby věděli, kam stahovat zprávy a odesílat zprávy.</string> <string name="public_chat_relays_title">Rele</string> <string name="public_chat_title">Metadata veřejného chatu</string> <string name="public_chats">Veřejné chaty</string> @@ -4684,10 +4667,7 @@ <string name="relay_auth_search_keywords">auth autentizace relay podepsat ověřit nip-42 identita</string> <string name="relay_auth_settings_title">Autentizace relay</string> <string name="relay_chat">Relé chat</string> - <string name="relay_chat_explainer">Relay chaty jsou chatové skupiny ovládané jejich domácí relé. - Jsou viditelné pro všechny na Nostru a každý se jich může zúčastnit. - Jsou skvělé pro otevřené komunity kolem konkrétních témat. Některé z těchto skupin jsou efemérní - a proto zprávy časem mizí</string> + <string name="relay_chat_explainer">Relay chaty jsou chatové skupiny ovládané jejich domácí relé. Jsou viditelné pro všechny na Nostru a každý se jich může zúčastnit. Jsou skvělé pro otevřené komunity kolem konkrétních témat. Některé z těchto skupin jsou efemérní a proto zprávy časem mizí</string> <string name="relay_chat_title">Relé chaty</string> <string name="relay_explain_account_data">Váš vlastní profil, nastavení a koncepty na vašich domovských relayích.</string> <string name="relay_explain_community_chats">Relaye, na které každá komunita publikuje své roviny.</string> diff --git a/commonsUI/src/commonMain/composeResources/values-de-rDE/strings.xml b/commonsUI/src/commonMain/composeResources/values-de-rDE/strings.xml index 076f687129..17617a8ae7 100644 --- a/commonsUI/src/commonMain/composeResources/values-de-rDE/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-de-rDE/strings.xml @@ -361,7 +361,7 @@ <string name="copy_user_id">Benutzer-ID kopieren</string> <string name="unblock_user">Benutzer freigeben</string> <string name="user_is_blocked_hidden">Dieser Nutzer ist blockiert/stummgeschaltet. Seine Beiträge sind ausgeblendet.</string> - <string name="npub_hex_username">npub, Benutzername, Text </string> + <string name="npub_hex_username">npub, Benutzername, Text</string> <string name="clear">Löschen</string> <string name="app_logo">App-Logo</string> <string name="nsec_npub_hex_private_key">nsec.. oder npub..</string> @@ -463,13 +463,8 @@ <string name="mark_all_known_as_read">Alle Kontakte als gelesen markieren</string> <string name="mark_all_new_as_read">Alle Unbekannten als gelesen markieren</string> <string name="mark_all_as_read">Alle als gelesen markieren</string> - <string name="account_backup_tips2_md"> ## Tipps zur Schlüsselsicherheit - \n\nDein Konto ist durch einen privaten Schlüssel gesichert. Der Schlüssel ist eine lange Zeichenfolge, die mit **nsec1** beginnt. Jeder, der Zugriff auf diesen privaten Schlüssel hat, kann in deinem Namen posten und dein Profil ändern. - \n\n- Lege deinen privaten Schlüssel **nicht** auf einer Website oder in einer Software ab, der du nicht vertraust. - \n- Die Entwickler von Amethyst werden dich **niemals** nach deinem privaten Schlüssel fragen. - \n- **Bewahre** eine sichere Kopie deines privaten Schlüssels zur Kontowiederherstellung auf. Wir empfehlen die Verwendung eines Passwort-Managers.</string> - <string name="account_backup_tips3_md"> Für zusätzliche Sicherheit kannst du deinen Schlüssel mit einem Passwort verschlüsseln. Dieser Schlüssel beginnt mit **ncryptsec1** und kann ohne dein Passwort nicht verwendet werden. - \n\nWenn du dein Passwort verlierst, kannst du deinen Schlüssel nicht wiederherstellen.</string> + <string name="account_backup_tips2_md">## Tipps zur Schlüsselsicherheit \n\nDein Konto ist durch einen privaten Schlüssel gesichert. Der Schlüssel ist eine lange Zeichenfolge, die mit **nsec1** beginnt. Jeder, der Zugriff auf diesen privaten Schlüssel hat, kann in deinem Namen posten und dein Profil ändern. \n\n- Lege deinen privaten Schlüssel **nicht** auf einer Website oder in einer Software ab, der du nicht vertraust. \n- Die Entwickler von Amethyst werden dich **niemals** nach deinem privaten Schlüssel fragen. \n- **Bewahre** eine sichere Kopie deines privaten Schlüssels zur Kontowiederherstellung auf. Wir empfehlen die Verwendung eines Passwort-Managers.</string> + <string name="account_backup_tips3_md">Für zusätzliche Sicherheit kannst du deinen Schlüssel mit einem Passwort verschlüsseln. Dieser Schlüssel beginnt mit **ncryptsec1** und kann ohne dein Passwort nicht verwendet werden. \n\nWenn du dein Passwort verlierst, kannst du deinen Schlüssel nicht wiederherstellen.</string> <string name="backup_keys_nudge_title">Sichere deine Schlüssel</string> <string name="backup_keys_nudge_body">Dein geheimer Schlüssel ist der einzige Weg zu diesem Konto. Wenn du ihn verlierst, kann er nie wiederhergestellt werden. Speichere ihn jetzt an einem sicheren Ort.</string> <string name="backup_keys_nudge_backup_now">Jetzt sichern</string> @@ -1584,9 +1579,7 @@ <string name="push_server_none_explainer">Push-Benachrichtigungen deaktivieren</string> <string name="push_server_uses_app_explainer">Verwendet die App %1$s</string> <string name="push_server_install_app">Push-Benachrichtigungen einrichten</string> - <string name="push_server_install_app_description"> Um Push-Benachrichtigungen zu erhalten, installiere eine App, die [Unified Push](https://unifiedpush.org/) unterstützt, wie zum Beispiel [Ntfy](https://ntfy.sh/). - Nach der Installation wähle in den Einstellungen die App aus, die du verwenden möchtest. - </string> + <string name="push_server_install_app_description">Um Push-Benachrichtigungen zu erhalten, installiere eine App, die [Unified Push](https://unifiedpush.org/) unterstützt, wie zum Beispiel [Ntfy](https://ntfy.sh/). Nach der Installation wähle in den Einstellungen die App aus, die du verwenden möchtest.</string> <string name="thread_title">Faden</string> <string name="send_the_seller_a_message">Dem Verkäufer eine Nachricht senden</string> <string name="hi_there_is_this_still_available">Hallo, ist das noch verfügbar?</string> @@ -3323,14 +3316,7 @@ <string name="could_not_fetch_invoice_from_details">Rechnung konnte nicht von %1$s abgerufen werden: %2$s</string> <string name="could_not_open_the_compressed_file">Konnte die komprimierte Datei nicht öffnen</string> <string name="couldnt_find_nwc_wallets">Wallet nicht gefunden</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst konnte keine Wallet-Apps finden, die Nostr Wallet Connect (NWC) unterstützen. -\n\nWas du tun kannst: -\n\t1. NWC-Unterstützung überprüfen: Stelle sicher, dass deine bevorzugte Wallet-App mit Nostr Wallet Connect kompatibel ist. -\n\t2. Verbindung erstellen: Öffne deine Wallet-App und erstelle eine neue NWC-Verbindung. -\n\t3. Mit Amethyst verknüpfen: Deine Wallet bietet normalerweise drei Optionen: -\n\t\t3.1 Automatische Verknüpfung: Eine Schaltfläche zum automatischen Öffnen und Verbinden mit Amethyst. Klicke darauf, öffne Amethyst und speichere auf dem neuen Bildschirm. -\n\t\t3.2 Manuelle Verknüpfung: Eine Verbindungs-URI, die du kopieren und über das Einfügesymbol auf diesem Bildschirm einfügen kannst. -\n\t\t3.3 QR-Code: Die Wallet erstellt einen QR-Code, den du über die QR-Schaltfläche auf diesem Bildschirm scannen kannst.</string> + <string name="couldnt_find_nwc_wallets_description">Amethyst konnte keine Wallet-Apps finden, die Nostr Wallet Connect (NWC) unterstützen. \n\nWas du tun kannst: \n\t1. NWC-Unterstützung überprüfen: Stelle sicher, dass deine bevorzugte Wallet-App mit Nostr Wallet Connect kompatibel ist. \n\t2. Verbindung erstellen: Öffne deine Wallet-App und erstelle eine neue NWC-Verbindung. \n\t3. Mit Amethyst verknüpfen: Deine Wallet bietet normalerweise drei Optionen: \n\t\t3.1 Automatische Verknüpfung: Eine Schaltfläche zum automatischen Öffnen und Verbinden mit Amethyst. Klicke darauf, öffne Amethyst und speichere auf dem neuen Bildschirm. \n\t\t3.2 Manuelle Verknüpfung: Eine Verbindungs-URI, die du kopieren und über das Einfügesymbol auf diesem Bildschirm einfügen kannst. \n\t\t3.3 QR-Code: Die Wallet erstellt einen QR-Code, den du über die QR-Schaltfläche auf diesem Bildschirm scannen kannst.</string> <string name="create">Erstellen</string> <string name="d">t</string> <string name="danger_zone">Gefahrenzone</string> @@ -4427,11 +4413,8 @@ <string name="proxy_section">Proxy-Relays</string> <string name="proxy_section_explainer">Aggregator-Relays wie filter.nostr.wine, von denen die App deine Feeds herunterlädt</string> <string name="public_chat">Öffentlicher Chat</string> - <string name="public_chat_explainer">Öffentliche Chats sind für jeden auf Nostr sichtbar und jeder - kann daran teilnehmen. Sie eignen sich hervorragend für offene Gemeinschaften rund um bestimmte Themen. - Moderation kann durch Löschen von Beiträgen auf den Relays gesteuert werden</string> - <string name="public_chat_relays_explainer">Füge zwischen 1–3 Relays ein, die diese Gruppe beherbergen. - Nostr Clients verwenden diese Einstellung, um zu wissen, woher sie Nachrichten herunterladen und an welche gesendet werden sollen.</string> + <string name="public_chat_explainer">Öffentliche Chats sind für jeden auf Nostr sichtbar und jeder kann daran teilnehmen. Sie eignen sich hervorragend für offene Gemeinschaften rund um bestimmte Themen. Moderation kann durch Löschen von Beiträgen auf den Relays gesteuert werden</string> + <string name="public_chat_relays_explainer">Füge zwischen 1–3 Relays ein, die diese Gruppe beherbergen. Nostr Clients verwenden diese Einstellung, um zu wissen, woher sie Nachrichten herunterladen und an welche gesendet werden sollen.</string> <string name="public_chat_relays_title">Relays</string> <string name="public_chat_title">Öffentliche Chat Metadaten</string> <string name="public_chats">Öffentliche Chats</string> @@ -4496,10 +4479,7 @@ <string name="relay_auth_search_keywords">auth authentifizierung relay signieren verifizieren nip-42 identität</string> <string name="relay_auth_settings_title">Relay-Authentifizierung</string> <string name="relay_chat">Relay-Chat</string> - <string name="relay_chat_explainer">Relay-Chats sind Chatgruppen, die von ihrem Home-Relay gesteuert werden. - Sie sind für jeden auf Nostr sichtbar und jeder kann an ihnen teilnehmen. - Sie eignen sich hervorragend für offene Communities rund um bestimmte Themen. Einige dieser Gruppen sind kurzsichtig - und daher verschwinden Chat-Nachrichten im Laufe der Zeit</string> + <string name="relay_chat_explainer">Relay-Chats sind Chatgruppen, die von ihrem Home-Relay gesteuert werden. Sie sind für jeden auf Nostr sichtbar und jeder kann an ihnen teilnehmen. Sie eignen sich hervorragend für offene Communities rund um bestimmte Themen. Einige dieser Gruppen sind kurzsichtig und daher verschwinden Chat-Nachrichten im Laufe der Zeit</string> <string name="relay_chat_title">Relay-Chats</string> <string name="relay_explain_account_data">Dein eigenes Profil, deine Einstellungen und Entwürfe auf deinen Heim-Relays.</string> <string name="relay_explain_community_chats">Die Relays, auf denen jede Community ihre Planes veröffentlicht.</string> diff --git a/commonsUI/src/commonMain/composeResources/values-de/strings.xml b/commonsUI/src/commonMain/composeResources/values-de/strings.xml index fe28311d40..a1bc04624c 100644 --- a/commonsUI/src/commonMain/composeResources/values-de/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-de/strings.xml @@ -211,7 +211,7 @@ <string name="copy_user_id">Benutzer-ID kopieren</string> <string name="unblock_user">Benutzer freigeben</string> <string name="user_is_blocked_hidden">Dieser Nutzer ist blockiert/stummgeschaltet. Seine Beiträge sind ausgeblendet.</string> - <string name="npub_hex_username">Npub, Hex, Benutzername </string> + <string name="npub_hex_username">Npub, Hex, Benutzername</string> <string name="clear">Löschen</string> <string name="app_logo">App-Logo</string> <string name="nsec_npub_hex_private_key">Nsec / Npub / Hex-Privatschlüssel</string> @@ -219,9 +219,7 @@ <string name="show_password">Passwort anzeigen</string> <string name="i_accept_the">Ich akzeptiere die </string> <string name="terms_of_use">Nutzungsbedingungen</string> - <string name="acceptance_of_terms_is_required">Die Akzept - -anz der Bedingungen ist erforderlich</string> + <string name="acceptance_of_terms_is_required">Die Akzept anz der Bedingungen ist erforderlich</string> <string name="login">Anmeldung</string> <string name="sign_up">Anmelden</string> <string name="create_account">Konto erstellen</string> @@ -261,13 +259,8 @@ anz der Bedingungen ist erforderlich</string> <string name="mark_all_known_as_read">Alle als bekannt markieren</string> <string name="mark_all_new_as_read">Alle als neu markieren</string> <string name="mark_all_as_read">Alle als gelesen markieren</string> - <string name="account_backup_tips2_md"> ## Schlüsselsicherheits-Tipps - \n\nIhr Konto ist durch einen geheimen Schlüssel gesichert. Der Schlüssel ist eine lange Zeichenfolge, die mit **nsec1** beginnt. Jeder, der Zugriff auf diesen geheimen Schlüssel hat, kann Ihre Identität posten und ändern. - \n\n- Legen Sie Ihren geheimen Schlüssel **nicht** auf eine Website oder in eine Software, der Sie nicht vertrauen. - \n- Die Entwickler von Amethyst werden Sie **niemals** nach Ihrem geheimen Schlüssel fragen. - \n- **Bewahren Sie** eine sichere Sicherung Ihres geheimen Schlüssels zur Kontowiederherstellung auf. Wir empfehlen die Verwendung eines Passwort-Managers.</string> - <string name="account_backup_tips3_md"> Für zusätzliche Sicherheit können Sie Ihren Schlüssel mit einem Passwort verschlüsseln. Dieser Schlüssel beginnt mit **ncryptsec1** und kann ohne Ihr Passwort nicht verwendet werden. - \n\nWenn Sie Ihr Passwort verlieren, können Sie Ihren Schlüssel nicht wiederherstellen.</string> + <string name="account_backup_tips2_md">## Schlüsselsicherheits-Tipps \n\nIhr Konto ist durch einen geheimen Schlüssel gesichert. Der Schlüssel ist eine lange Zeichenfolge, die mit **nsec1** beginnt. Jeder, der Zugriff auf diesen geheimen Schlüssel hat, kann Ihre Identität posten und ändern. \n\n- Legen Sie Ihren geheimen Schlüssel **nicht** auf eine Website oder in eine Software, der Sie nicht vertrauen. \n- Die Entwickler von Amethyst werden Sie **niemals** nach Ihrem geheimen Schlüssel fragen. \n- **Bewahren Sie** eine sichere Sicherung Ihres geheimen Schlüssels zur Kontowiederherstellung auf. Wir empfehlen die Verwendung eines Passwort-Managers.</string> + <string name="account_backup_tips3_md">Für zusätzliche Sicherheit können Sie Ihren Schlüssel mit einem Passwort verschlüsseln. Dieser Schlüssel beginnt mit **ncryptsec1** und kann ohne Ihr Passwort nicht verwendet werden. \n\nWenn Sie Ihr Passwort verlieren, können Sie Ihren Schlüssel nicht wiederherstellen.</string> <string name="encrypt_and_copy_my_secret_key">Verschlüsseln und kopieren Sie meinen geheimen Schlüssel</string> <string name="badge_award_image_for">Auszeichnungsbild für %1$s</string> <string name="badge_award_image">Auszeichnungsbild</string> @@ -960,9 +953,7 @@ anz der Bedingungen ist erforderlich</string> <string name="push_server_none_explainer">Push-Benachrichtigungen deaktivieren</string> <string name="push_server_uses_app_explainer">Verwendet die App %1$s</string> <string name="push_server_install_app">Push-Benachrichtigungen einrichten</string> - <string name="push_server_install_app_description"> Um Push-Benachrichtigungen zu erhalten, installieren Sie eine App, die [Unified Push](https://unifiedpush.org/) unterstützt, wie zum Beispiel [Ntfy](https://ntfy.sh/). - Nach der Installation wählen Sie die App aus, die Sie in den Einstellungen verwenden möchten. - </string> + <string name="push_server_install_app_description">Um Push-Benachrichtigungen zu erhalten, installieren Sie eine App, die [Unified Push](https://unifiedpush.org/) unterstützt, wie zum Beispiel [Ntfy](https://ntfy.sh/). Nach der Installation wählen Sie die App aus, die Sie in den Einstellungen verwenden möchten.</string> <string name="thread_title">Faden</string> <string name="send_the_seller_a_message">Dem Verkäufer eine Nachricht senden</string> <string name="hi_there_is_this_still_available">Hallo, ist das noch verfügbar?</string> @@ -2064,14 +2055,7 @@ anz der Bedingungen ist erforderlich</string> <string name="could_not_fetch_invoice_from_details">Rechnung konnte nicht von %1$s abgerufen werden: %2$s</string> <string name="could_not_open_the_compressed_file">Konnte die komprimierte Datei nicht öffnen</string> <string name="couldnt_find_nwc_wallets">Wallet nicht gefunden</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst konnte keine Wallet-Apps finden, die Nostr Wallet Connect (NWC) unterstützen. -\n\nWas du tun kannst: -\n\t1. NWC-Unterstützung überprüfen: Stelle sicher, dass deine bevorzugte Wallet-App mit Nostr Wallet Connect kompatibel ist. -\n\t2. Verbindung erstellen: Öffne deine Wallet-App und erstelle eine neue NWC-Verbindung. -\n\t3. Mit Amethyst verknüpfen: Deine Wallet bietet normalerweise drei Optionen: -\n\t\t3.1 Automatische Verknüpfung: Eine Schaltfläche zum automatischen Öffnen und Verbinden mit Amethyst. Klicke darauf, öffne Amethyst und speichere auf dem neuen Bildschirm. -\n\t\t3.2 Manuelle Verknüpfung: Eine Verbindungs-URI, die du kopieren und über das Einfügesymbol auf diesem Bildschirm einfügen kannst. -\n\t\t3.3 QR-Code: Die Wallet erstellt einen QR-Code, den du über die QR-Schaltfläche auf diesem Bildschirm scannen kannst.</string> + <string name="couldnt_find_nwc_wallets_description">Amethyst konnte keine Wallet-Apps finden, die Nostr Wallet Connect (NWC) unterstützen. \n\nWas du tun kannst: \n\t1. NWC-Unterstützung überprüfen: Stelle sicher, dass deine bevorzugte Wallet-App mit Nostr Wallet Connect kompatibel ist. \n\t2. Verbindung erstellen: Öffne deine Wallet-App und erstelle eine neue NWC-Verbindung. \n\t3. Mit Amethyst verknüpfen: Deine Wallet bietet normalerweise drei Optionen: \n\t\t3.1 Automatische Verknüpfung: Eine Schaltfläche zum automatischen Öffnen und Verbinden mit Amethyst. Klicke darauf, öffne Amethyst und speichere auf dem neuen Bildschirm. \n\t\t3.2 Manuelle Verknüpfung: Eine Verbindungs-URI, die du kopieren und über das Einfügesymbol auf diesem Bildschirm einfügen kannst. \n\t\t3.3 QR-Code: Die Wallet erstellt einen QR-Code, den du über die QR-Schaltfläche auf diesem Bildschirm scannen kannst.</string> <string name="create">Erstellen</string> <string name="d">t</string> <string name="danger_zone">Gefahrenzone</string> @@ -2274,9 +2258,7 @@ anz der Bedingungen ist erforderlich</string> <string name="http_status_508">Schleife erkannt - Der Server erkennt eine Endlosschleife, während er die Anfrage bearbeitet</string> <string name="http_status_511">Netzwerk-Authentifizierung erforderlich - Der Client muss authentifiziert werden, um auf das Netzwerk zuzugreifen</string> <string name="illegal_behavior">Illegales Verhalten</string> - <string name="image_saved_to_the_gallery">Bild in der Gal - -erie gespeichert</string> + <string name="image_saved_to_the_gallery">Bild in der Gal erie gespeichert</string> <string name="impersonation">Vortäuschung</string> <string name="import_follows_search_keywords">Kontakte, folgen, Folgeliste, importieren</string> <string name="inbox_relays_title">Eingehende Relays</string> @@ -2777,11 +2759,8 @@ erie gespeichert</string> <string name="proxy_section">Proxy-Relays</string> <string name="proxy_section_explainer">Aggregator-Relays wie filter.nostr.wine, von denen die App deine Feeds herunterlädt</string> <string name="public_chat">Öffentlicher Chat</string> - <string name="public_chat_explainer">Öffentliche Chats sind für jeden auf Nostr sichtbar und jeder - kann daran teilnehmen. Sie eignen sich hervorragend für offene Gemeinschaften rund um bestimmte Themen. - Moderation kann durch Löschen von Beiträgen auf den Relais gesteuert werden</string> - <string name="public_chat_relays_explainer">Fügen Sie zwischen 1–3 Relais ein, die diese Gruppe beherbergen. - Nostr Clients verwenden diese Einstellung, um zu wissen, woher sie Nachrichten herunterladen und an welche gesendet werden sollen.</string> + <string name="public_chat_explainer">Öffentliche Chats sind für jeden auf Nostr sichtbar und jeder kann daran teilnehmen. Sie eignen sich hervorragend für offene Gemeinschaften rund um bestimmte Themen. Moderation kann durch Löschen von Beiträgen auf den Relais gesteuert werden</string> + <string name="public_chat_relays_explainer">Fügen Sie zwischen 1–3 Relais ein, die diese Gruppe beherbergen. Nostr Clients verwenden diese Einstellung, um zu wissen, woher sie Nachrichten herunterladen und an welche gesendet werden sollen.</string> <string name="public_chat_relays_title">Relais</string> <string name="public_chat_title">Öffentliche Chat Metadaten</string> <string name="public_chats">Öffentliche Chats</string> @@ -2808,10 +2787,7 @@ erie gespeichert</string> <string name="read_only_user">Nur-Lese-Benutzer</string> <string name="record_a_message_description">Zum Aufnehmen einer Nachricht gedrückt halten</string> <string name="relay_chat">Relay-Chat</string> - <string name="relay_chat_explainer">Relaischats sind Chatgruppen, die von ihrem Home-Relais gesteuert werden. - Sie sind für jeden auf Nostr sichtbar und jeder kann an ihnen teilnehmen. - Sie eignen sich hervorragend für offene Communities rund um bestimmte Themen. Einige dieser Gruppen sind kurzsichtig - und daher verschwinden Chat-Nachrichten im Laufe der Zeit</string> + <string name="relay_chat_explainer">Relaischats sind Chatgruppen, die von ihrem Home-Relais gesteuert werden. Sie sind für jeden auf Nostr sichtbar und jeder kann an ihnen teilnehmen. Sie eignen sich hervorragend für offene Communities rund um bestimmte Themen. Einige dieser Gruppen sind kurzsichtig und daher verschwinden Chat-Nachrichten im Laufe der Zeit</string> <string name="relay_chat_title">Relay-Chats</string> <string name="relay_management_tab_events">Ereignisse</string> <string name="relay_management_tab_ips">IPs</string> diff --git a/commonsUI/src/commonMain/composeResources/values-el-rGR/strings.xml b/commonsUI/src/commonMain/composeResources/values-el-rGR/strings.xml index 6569dd09c3..ffe6a08835 100644 --- a/commonsUI/src/commonMain/composeResources/values-el-rGR/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-el-rGR/strings.xml @@ -271,15 +271,8 @@ <string name="mark_all_known_as_read">Σήμανση όλων των γνωστοποιήσεων ως αναγνωσμένων</string> <string name="mark_all_new_as_read">Σήμανση όλων ως αναγνωσμένων</string> <string name="mark_all_as_read">Σήμανση όλων ως αναγνωσμένα</string> - <string name="account_backup_tips2_md"> ## Συμβουλές ασφαλείας και δημιουργίας αντιγράφων ασφαλείας κλειδιού - \n\nΟ λογαριασμός σου ασφαλίζεται με ένα μυστικό κλειδί. Το κλειδί είναι μια μακρά ακολουθία χαρακτήρων που ξεκινά με **nsec1**. Όποιος έχει πρόσβαση σε αυτό το μυστικό κλειδί μπορεί να δημοσιεύει και να αλλάζει την ταυτότητά σου. - \n\n- **Μην** βάζεις το μυστικό σου κλειδί σε κανέναν ιστότοπο ή λογισμικό που δεν εμπιστεύεσαι. - \n- Οι προγραμματιστές του Amethyst **δεν** θα ζητήσουν ποτέ το μυστικό σου κλειδί. - \n- **Διατήρησε** ένα ασφαλές αντίγραφο ασφαλείας του μυστικού σου κλειδιού για ανάκτηση λογαριασμού. Συνιστούμε τη χρήση διαχειριστή κωδικών πρόσβασης. - </string> - <string name="account_backup_tips3_md"> Για πρόσθετη ασφάλεια, μπορείς να κρυπτογραφήσεις το κλειδί σου με κωδικό πρόσβασης. Αυτό το κλειδί ξεκινά με **ncryptsec1** και δεν μπορεί να χρησιμοποιηθεί χωρίς τον κωδικό σου. - \n\nΑν χάσεις τον κωδικό σου, δεν θα μπορείς να ανακτήσεις το κλειδί σου. - </string> + <string name="account_backup_tips2_md">## Συμβουλές ασφαλείας και δημιουργίας αντιγράφων ασφαλείας κλειδιού \n\nΟ λογαριασμός σου ασφαλίζεται με ένα μυστικό κλειδί. Το κλειδί είναι μια μακρά ακολουθία χαρακτήρων που ξεκινά με **nsec1**. Όποιος έχει πρόσβαση σε αυτό το μυστικό κλειδί μπορεί να δημοσιεύει και να αλλάζει την ταυτότητά σου. \n\n- **Μην** βάζεις το μυστικό σου κλειδί σε κανέναν ιστότοπο ή λογισμικό που δεν εμπιστεύεσαι. \n- Οι προγραμματιστές του Amethyst **δεν** θα ζητήσουν ποτέ το μυστικό σου κλειδί. \n- **Διατήρησε** ένα ασφαλές αντίγραφο ασφαλείας του μυστικού σου κλειδιού για ανάκτηση λογαριασμού. Συνιστούμε τη χρήση διαχειριστή κωδικών πρόσβασης.</string> + <string name="account_backup_tips3_md">Για πρόσθετη ασφάλεια, μπορείς να κρυπτογραφήσεις το κλειδί σου με κωδικό πρόσβασης. Αυτό το κλειδί ξεκινά με **ncryptsec1** και δεν μπορεί να χρησιμοποιηθεί χωρίς τον κωδικό σου. \n\nΑν χάσεις τον κωδικό σου, δεν θα μπορείς να ανακτήσεις το κλειδί σου.</string> <string name="encrypt_and_copy_my_secret_key">Κρυπτογράφηση και αντιγραφή του μυστικού μου κλειδιού</string> <string name="badge_award_image_for">Σήμα βράβευσης για %1$s</string> <string name="badge_award_image">"Εικόνα βραβείου παρασήμου</string> @@ -934,8 +927,7 @@ <string name="moderators">Συντονιστές</string> <string name="login_with_external_signer">Σύνδεση με Amber</string> <string name="status_update">Ενημέρωση κατάστασης</string> - <string name="poll_zap_value_min_max_explainer">Η ψηφοφορία σε μια δημοσίευση Δημοσκόπησης γίνεται με zaps. Τα zaps είναι μικροπληρωμές σε satoshi (sats). Τα sats είναι υποδιαίρεση του Bitcoin. Κάθε Bitcoin έχει 100.000.000 sats. -Η επιλογή με τα περισσότερα zaps (δηλαδή τα περισσότερα sats) είναι η επικρατέστερη. Μπορείτε να ορίσετε ένα ελάχιστο ποσό zap για να αποφύγετε την ψήφο spammers αλλά και ένα μέγιστο ποσό ώστε να αποφύγετε μεγάλους zappers (χρήστες που στέλνουν zaps) που επιθυμούν να χειραγωγήσουν την ψηφοφορία. Χρησιμοποιήστε το ίδιο ποσό και στα δύο πεδία για να βεβαιωθείτε ότι κάθε ψήφος αποτιμάται το ίδιο. Ή αφήστε τα πεδία κενά για να αποδεχτείτε οποιοδήποτε ποσό.</string> + <string name="poll_zap_value_min_max_explainer">Η ψηφοφορία σε μια δημοσίευση Δημοσκόπησης γίνεται με zaps. Τα zaps είναι μικροπληρωμές σε satoshi (sats). Τα sats είναι υποδιαίρεση του Bitcoin. Κάθε Bitcoin έχει 100.000.000 sats. Η επιλογή με τα περισσότερα zaps (δηλαδή τα περισσότερα sats) είναι η επικρατέστερη. Μπορείτε να ορίσετε ένα ελάχιστο ποσό zap για να αποφύγετε την ψήφο spammers αλλά και ένα μέγιστο ποσό ώστε να αποφύγετε μεγάλους zappers (χρήστες που στέλνουν zaps) που επιθυμούν να χειραγωγήσουν την ψηφοφορία. Χρησιμοποιήστε το ίδιο ποσό και στα δύο πεδία για να βεβαιωθείτε ότι κάθε ψήφος αποτιμάται το ίδιο. Ή αφήστε τα πεδία κενά για να αποδεχτείτε οποιοδήποτε ποσό.</string> <string name="error_dialog_talk_to_user">Μήνυμα στον Χρήστη</string> <string name="error_dialog_talk_to_user_name">Μήνυμα σε %1$s</string> <string name="error_dialog_button_ok">ΟΚ</string> @@ -963,9 +955,7 @@ <string name="push_server_none_explainer">Απενεργοποίηση Ειδοποιήσεων Push</string> <string name="push_server_uses_app_explainer">Χρήση εφαρμογής %1$s</string> <string name="push_server_install_app">Ρυθμίσεις Ειδοποιήσεων Push</string> - <string name="push_server_install_app_description"> Για να λαμβάνετε ειδοποιήσεις Push, εγκαταστήστε οποιαδήποτε εφαρμογή τις υποστηρίζει [Unified Push](https://unifiedpush.org/), όπως πχ το [Ntfy](https://ntfy.sh/). - Μετά την εγκατάσταση, επιλέξτε την εφαρμογή που θέλετε να χρησιμοποιήσετε από τις Ρυθμίσεις. - </string> + <string name="push_server_install_app_description">Για να λαμβάνετε ειδοποιήσεις Push, εγκαταστήστε οποιαδήποτε εφαρμογή τις υποστηρίζει [Unified Push](https://unifiedpush.org/), όπως πχ το [Ntfy](https://ntfy.sh/). Μετά την εγκατάσταση, επιλέξτε την εφαρμογή που θέλετε να χρησιμοποιήσετε από τις Ρυθμίσεις.</string> <string name="thread_title">Νήμα</string> <string name="send_the_seller_a_message">Στείλτε μήνυμα στον πωλητή</string> <string name="hi_there_is_this_still_available">Γεια σου, είναι ακόμα διαθέσιμο;</string> @@ -2008,15 +1998,7 @@ <string name="could_not_fetch_invoice_from_details">Δεν ήταν δυνατή η λήψη τιμολογίου από %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Δεν ήταν δυνατό το άνοιγμα του συμπιεσμένου αρχείου</string> <string name="couldnt_find_nwc_wallets">Το πορτοφόλι δεν βρέθηκε</string> - <string name="couldnt_find_nwc_wallets_description">Το Amethyst δεν βρήκε εφαρμογές πορτοφολιού που υποστηρίζουν Nostr Wallet Connect (NWC). -\n\nΤι μπορείτε να κάνετε: -\n\t1. Επαλήθευση υποστήριξης NWC: Βεβαιωθείτε ότι η προτιμώμενη εφαρμογή πορτοφολιού σας είναι συμβατή με Nostr Wallet Connect. -\n\t2. Δημιουργία σύνδεσης: Μεταβείτε στην εφαρμογή πορτοφολιού σας και δημιουργήστε μια νέα σύνδεση NWC. -\n\t3. Σύνδεση με το Amethyst: Το πορτοφόλι σας συνήθως θα σας δώσει τρεις επιλογές: -\n\t\t3.1 Αυτόματη σύνδεση: Ένα κουμπί για αυτόματο άνοιγμα και σύνδεση με το Amethyst. Αν ναι, κάντε κλικ σε αυτό το κουμπί, ανοίξτε το Amethyst και πατήστε αποθήκευση στη νέα οθόνη. -\n\t\t3.2 Χειροκίνητη σύνδεση: Ένα URI σύνδεσης που μπορείτε να αντιγράψετε και να κάνετε κλικ στο εικονίδιο επικόλλησης σε αυτή την οθόνη για απευθείας εισαγωγή. -\n\t\t3.3 Κωδικός QR: Το πορτοφόλι δημιουργεί κωδικό QR τον οποίο μπορείτε να σαρώσετε από το κουμπί κωδικού QR σε αυτή την οθόνη. - </string> + <string name="couldnt_find_nwc_wallets_description">Το Amethyst δεν βρήκε εφαρμογές πορτοφολιού που υποστηρίζουν Nostr Wallet Connect (NWC). \n\nΤι μπορείτε να κάνετε: \n\t1. Επαλήθευση υποστήριξης NWC: Βεβαιωθείτε ότι η προτιμώμενη εφαρμογή πορτοφολιού σας είναι συμβατή με Nostr Wallet Connect. \n\t2. Δημιουργία σύνδεσης: Μεταβείτε στην εφαρμογή πορτοφολιού σας και δημιουργήστε μια νέα σύνδεση NWC. \n\t3. Σύνδεση με το Amethyst: Το πορτοφόλι σας συνήθως θα σας δώσει τρεις επιλογές: \n\t\t3.1 Αυτόματη σύνδεση: Ένα κουμπί για αυτόματο άνοιγμα και σύνδεση με το Amethyst. Αν ναι, κάντε κλικ σε αυτό το κουμπί, ανοίξτε το Amethyst και πατήστε αποθήκευση στη νέα οθόνη. \n\t\t3.2 Χειροκίνητη σύνδεση: Ένα URI σύνδεσης που μπορείτε να αντιγράψετε και να κάνετε κλικ στο εικονίδιο επικόλλησης σε αυτή την οθόνη για απευθείας εισαγωγή. \n\t\t3.3 Κωδικός QR: Το πορτοφόλι δημιουργεί κωδικό QR τον οποίο μπορείτε να σαρώσετε από το κουμπί κωδικού QR σε αυτή την οθόνη.</string> <string name="create">Δημιουργία</string> <string name="d">η</string> <string name="danger_zone">Επικίνδυνη ζώνη</string> @@ -2653,11 +2635,8 @@ <string name="proxy_section">Relay Διαμεσολαβητή</string> <string name="proxy_section_explainer">Relay συγκεντρωτές που η εφαρμογή πρέπει να χρησιμοποιεί για να κατεβάσει τις τροφοδοσίες σας, όπως filter.nostr.wine. Αυτό αντικαθιστά το μοντέλο outbox και θα κάνει την εφαρμογή να συνδέεται μόνο στα relay στις λίστες σας.</string> <string name="public_chat">Δημόσια Συζήτηση</string> - <string name="public_chat_explainer">Οι δημόσιες συνομιλίες είναι ορατές σε όλους στο Nostr και οποιοσδήποτε - μπορεί να συμμετάσχει σε αυτές. Είναι ιδανικές για ανοιχτές κοινότητες γύρω από συγκεκριμένα θέματα. - Η εποπτεία μπορεί να ελεγχθεί με διαγραφή αναρτήσεων στα relay της</string> - <string name="public_chat_relays_explainer">Εισάγετε 1–3 relay που φιλοξενούν αυτή την ομάδα. - Οι πελάτες Nostr χρησιμοποιούν αυτή τη ρύθμιση για να γνωρίζουν πού να κατεβάσουν μηνύματα και πού να στέλνουν τα μηνύματά σου.</string> + <string name="public_chat_explainer">Οι δημόσιες συνομιλίες είναι ορατές σε όλους στο Nostr και οποιοσδήποτε μπορεί να συμμετάσχει σε αυτές. Είναι ιδανικές για ανοιχτές κοινότητες γύρω από συγκεκριμένα θέματα. Η εποπτεία μπορεί να ελεγχθεί με διαγραφή αναρτήσεων στα relay της</string> + <string name="public_chat_relays_explainer">Εισάγετε 1–3 relay που φιλοξενούν αυτή την ομάδα. Οι πελάτες Nostr χρησιμοποιούν αυτή τη ρύθμιση για να γνωρίζουν πού να κατεβάσουν μηνύματα και πού να στέλνουν τα μηνύματά σου.</string> <string name="public_chat_title">Μεταδεδομένα δημόσιας συνομιλίας</string> <string name="public_chats">Δημόσιες Συνομιλίες</string> <string name="public_home_section">Δημόσια Relay Εξερχόμενων/Αρχικής</string> @@ -2681,10 +2660,7 @@ <string name="read_only_user">Χρήστης μόνο για ανάγνωση</string> <string name="record_a_message_description">Πάτα παρατεταμένα για να καταγράψεις ένα μήνυμα</string> <string name="relay_chat">Συνομιλία relay</string> - <string name="relay_chat_explainer">Οι συνομιλίες relay είναι ομάδες συνομιλίας που ελέγχονται από το αρχικό τους relay. - Είναι ορατές σε όλους στο Nostr και οποιοσδήποτε μπορεί να συμμετάσχει σε αυτές. - Είναι ιδανικές για ανοιχτές κοινότητες γύρω από συγκεκριμένα θέματα. Ορισμένες από αυτές τις ομάδες είναι εφήμερες - και έτσι τα μηνύματα συνομιλίας εξαφανίζονται με την πάροδο του χρόνου</string> + <string name="relay_chat_explainer">Οι συνομιλίες relay είναι ομάδες συνομιλίας που ελέγχονται από το αρχικό τους relay. Είναι ορατές σε όλους στο Nostr και οποιοσδήποτε μπορεί να συμμετάσχει σε αυτές. Είναι ιδανικές για ανοιχτές κοινότητες γύρω από συγκεκριμένα θέματα. Ορισμένες από αυτές τις ομάδες είναι εφήμερες και έτσι τα μηνύματα συνομιλίας εξαφανίζονται με την πάροδο του χρόνου</string> <string name="relay_chat_title">Συνομιλίες relay</string> <string name="relay_management_tab_events">Συμβάντα</string> <string name="relay_management_tab_pubkeys">Χρήστες</string> diff --git a/commonsUI/src/commonMain/composeResources/values-eo-rUY/strings.xml b/commonsUI/src/commonMain/composeResources/values-eo-rUY/strings.xml index e55ddcf03c..35481e4172 100644 --- a/commonsUI/src/commonMain/composeResources/values-eo-rUY/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-eo-rUY/strings.xml @@ -225,7 +225,7 @@ <string name="copy_user_id">Kopii ID de uzanto</string> <string name="unblock_user">Malbloki Uzanton</string> <string name="user_is_blocked_hidden">Tiu ĉi uzanto estas blokita/silentita. Iliaj afiŝoj estas kaŝitaj.</string> - <string name="npub_hex_username">npub, hekso, uzantnomo </string> + <string name="npub_hex_username">npub, hekso, uzantnomo</string> <string name="clear">Klarigi</string> <string name="app_logo">Logo de Apo</string> <string name="nsec_npub_hex_private_key">nsec / npub / heksa privata ŝlosilo</string> @@ -273,15 +273,8 @@ <string name="mark_all_known_as_read">Marki ĉiujn Konatajn legitaj</string> <string name="mark_all_new_as_read">Marki ĉiujn Novajn legitaj</string> <string name="mark_all_as_read">Marki ĉiujn legitajn</string> - <string name="account_backup_tips2_md"> ## Konsiletoj pri Ŝlosila Sekurkopiao kaj Sekureco - \n\nVia konto estas sekurigita per sekreta ŝlosilo. La ŝlosilo estas longa sinsekvo de signoj komencanta per **nsec1**. Ĉiu kiu havas aliron al ĉi tiu sekreta ŝlosilo povas afiŝi kaj ŝanĝi vian identecon. - \n\n- **Ne** metu vian sekretan ŝlosilon en ajnan retejon aŭ programaron, kiun vi ne fidas. - \n- Amethyst-programistoj **neniam** petos vian sekretan ŝlosilon. - \n- **Faru** sekuran sekurkopion de via sekreta ŝlosilo por reakiro de konto. Ni rekomendas uzi pasvortan administrilon. - </string> - <string name="account_backup_tips3_md"> Por plia sekureco, vi povas ĉifri vian ŝlosilon per pasvorto. Ĉi tiu ŝlosilo komenciĝas per **ncryptsec1** kaj ne povas esti uzata sen via pasvorto. - \n\nSe vi perdas vian pasvorton, vi ne povos reakiri vian ŝlosilon. - </string> + <string name="account_backup_tips2_md">## Konsiletoj pri Ŝlosila Sekurkopiao kaj Sekureco \n\nVia konto estas sekurigita per sekreta ŝlosilo. La ŝlosilo estas longa sinsekvo de signoj komencanta per **nsec1**. Ĉiu kiu havas aliron al ĉi tiu sekreta ŝlosilo povas afiŝi kaj ŝanĝi vian identecon. \n\n- **Ne** metu vian sekretan ŝlosilon en ajnan retejon aŭ programaron, kiun vi ne fidas. \n- Amethyst-programistoj **neniam** petos vian sekretan ŝlosilon. \n- **Faru** sekuran sekurkopion de via sekreta ŝlosilo por reakiro de konto. Ni rekomendas uzi pasvortan administrilon.</string> + <string name="account_backup_tips3_md">Por plia sekureco, vi povas ĉifri vian ŝlosilon per pasvorto. Ĉi tiu ŝlosilo komenciĝas per **ncryptsec1** kaj ne povas esti uzata sen via pasvorto. \n\nSe vi perdas vian pasvorton, vi ne povos reakiri vian ŝlosilon.</string> <string name="encrypt_and_copy_my_secret_key">Ĉifri kaj kopii mian seketan ŝlosilon</string> <string name="badge_award_image_for">Ŝildeto-premia bildo por %1$s</string> <string name="badge_award_image">"Insigna premibildeto</string> @@ -971,9 +964,7 @@ <string name="push_server_none_explainer">Malaktivigi Puŝajn Sciigojn</string> <string name="push_server_uses_app_explainer">Uzas aplikaĵon %1$s</string> <string name="push_server_install_app">Puŝa Sciig-Agordo</string> - <string name="push_server_install_app_description"> Por ricevi puŝajn sciigojn, instalu ajnan aplikaĵon kiu subtenas [Unified Push](https://unifiedpush.org/), kiel ekzemple [Ntfy](https://ntfy.sh/). - Post instalado, elektu la aplikaĵon, kiun vi volas uzi en la Agordoj. - </string> + <string name="push_server_install_app_description">Por ricevi puŝajn sciigojn, instalu ajnan aplikaĵon kiu subtenas [Unified Push](https://unifiedpush.org/), kiel ekzemple [Ntfy](https://ntfy.sh/). Post instalado, elektu la aplikaĵon, kiun vi volas uzi en la Agordoj.</string> <string name="thread_title">Fadeno</string> <string name="send_the_seller_a_message">Sendi mesaĝon al la vendisto</string> <string name="hi_there_is_this_still_available">Saluton, ĉu ĉi tio ankoraŭ disponeblas?</string> @@ -2031,15 +2022,7 @@ <string name="could_not_fetch_invoice_from_details">Ne eblis preni fakturon de %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Ne eblis malfermi la kunpremitan dosieron</string> <string name="couldnt_find_nwc_wallets">Monujo ne trovita</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst ne povis trovi ajnajn monujo-aplikojn, kiuj subtenas Nostr Wallet Connect (NWC). -\n\nKion vi povas fari: -\n\t1. Kontrolu NWC-subtenon: Duoble kontrolu, ĉu via preferata monujo-aplikaĵo estas kongrua kun Nostr Wallet Connect. -\n\t2. Krei Konekton: Iru al via monujo-aplikaĵo kaj kreu novan NWC-konekton. -\n\t3. Ligi al Amethyst: Via monujo kutime donos al vi tri opciojn: -\n\t\t3.1 Aŭtomata Ligo: Butono por aŭtomate malfermi kaj konekti al Amethyst. Se jes, alklaku tiun butonon, malfermu Amethyst kaj alklaku konservi sur la nova ekrano. -\n\t\t3.2 Mana Ligo: Konekta URI, kiun vi povas kopii kaj alklaki la algluu-ikonon sur ĉi tiu ekrano por rekte enigi ĝin. -\n\t\t3.3 QR-Kodo: La monujo kreas QR-kodon, kiun vi povas legi per la QR-koda butono sur ĉi tiu ekrano. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst ne povis trovi ajnajn monujo-aplikojn, kiuj subtenas Nostr Wallet Connect (NWC). \n\nKion vi povas fari: \n\t1. Kontrolu NWC-subtenon: Duoble kontrolu, ĉu via preferata monujo-aplikaĵo estas kongrua kun Nostr Wallet Connect. \n\t2. Krei Konekton: Iru al via monujo-aplikaĵo kaj kreu novan NWC-konekton. \n\t3. Ligi al Amethyst: Via monujo kutime donos al vi tri opciojn: \n\t\t3.1 Aŭtomata Ligo: Butono por aŭtomate malfermi kaj konekti al Amethyst. Se jes, alklaku tiun butonon, malfermu Amethyst kaj alklaku konservi sur la nova ekrano. \n\t\t3.2 Mana Ligo: Konekta URI, kiun vi povas kopii kaj alklaki la algluu-ikonon sur ĉi tiu ekrano por rekte enigi ĝin. \n\t\t3.3 QR-Kodo: La monujo kreas QR-kodon, kiun vi povas legi per la QR-koda butono sur ĉi tiu ekrano.</string> <string name="create">Krei</string> <string name="d">t</string> <string name="danger_zone">Danĝera Zono</string> @@ -2701,11 +2684,8 @@ <string name="proxy_section">Prokuro-Relay-oj</string> <string name="proxy_section_explainer">Agregataj relay-oj, kiujn la aplikaĵo devas uzi por elŝuti viajn fluojn, ekzemple filter.nostr.wine. Ĉi tio anstataŭas la sendkesta modelon kaj igos la aplikaĵon konektiĝi nur al la relay-oj en viaj listoj.</string> <string name="public_chat">Publika Babilado</string> - <string name="public_chat_explainer">Publikaj babilejoj estas videblaj al ĉiuj en Nostr kaj ĉiu ajn - povas partopreni en ili. Ili estas bonegaj por malfermitaj komunumoj pri specifaj temoj. - Moderado povas esti kontrolata per forigo de afiŝoj sur siaj relays</string> - <string name="public_chat_relays_explainer">Enigu inter 1–3 relays, kiuj gastigas ĉi tiun grupon. - Nostr-klientoj uzas ĉi tiun agordon por scii, kie elŝuti mesaĝojn kaj kien sendi viajn mesaĝojn.</string> + <string name="public_chat_explainer">Publikaj babilejoj estas videblaj al ĉiuj en Nostr kaj ĉiu ajn povas partopreni en ili. Ili estas bonegaj por malfermitaj komunumoj pri specifaj temoj. Moderado povas esti kontrolata per forigo de afiŝoj sur siaj relays</string> + <string name="public_chat_relays_explainer">Enigu inter 1–3 relays, kiuj gastigas ĉi tiun grupon. Nostr-klientoj uzas ĉi tiun agordon por scii, kie elŝuti mesaĝojn kaj kien sendi viajn mesaĝojn.</string> <string name="public_chat_relays_title">Relay-oj</string> <string name="public_chat_title">Publikaj Babilejaj Metadatenoj</string> <string name="public_chats">Publikaj Babilejoj</string> @@ -2730,10 +2710,7 @@ <string name="read_only_user">Nur-lega uzanto</string> <string name="record_a_message_description">Alklaku kaj tenu por registri mesaĝon</string> <string name="relay_chat">Relay-Babilejo</string> - <string name="relay_chat_explainer">Relay-babilejoj estas babilaj grupoj kontrolataj de sia hejma relay. - Ili estas videblaj al ĉiuj en Nostr kaj ĉiu ajn povas partopreni en ili. - Ili estas bonegaj por malfermitaj komunumoj pri specifaj temoj. Kelkaj el ĉi tiuj grupoj estas efemeraj - kaj tial babilmesaĝoj malaperas kun la tempo</string> + <string name="relay_chat_explainer">Relay-babilejoj estas babilaj grupoj kontrolataj de sia hejma relay. Ili estas videblaj al ĉiuj en Nostr kaj ĉiu ajn povas partopreni en ili. Ili estas bonegaj por malfermitaj komunumoj pri specifaj temoj. Kelkaj el ĉi tiuj grupoj estas efemeraj kaj tial babilmesaĝoj malaperas kun la tempo</string> <string name="relay_chat_title">Relay-Babilejaroj</string> <string name="relay_management_tab_events">Eventoj</string> <string name="relay_management_tab_ips">IP-oj</string> diff --git a/commonsUI/src/commonMain/composeResources/values-eo/strings.xml b/commonsUI/src/commonMain/composeResources/values-eo/strings.xml index f9cf0c8844..ef18a4b455 100644 --- a/commonsUI/src/commonMain/composeResources/values-eo/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-eo/strings.xml @@ -211,7 +211,7 @@ <string name="copy_user_id">Kopii ID de uzanto</string> <string name="unblock_user">Malbloki Uzanton</string> <string name="user_is_blocked_hidden">Tiu ĉi uzanto estas blokita/silentita. Iliaj afiŝoj estas kaŝitaj.</string> - <string name="npub_hex_username">npub, hekso, uzantnomo </string> + <string name="npub_hex_username">npub, hekso, uzantnomo</string> <string name="clear">Klarigi</string> <string name="app_logo">Logo de Apo</string> <string name="nsec_npub_hex_private_key">nsec / npub / heksa privata ŝlosilo</string> @@ -259,17 +259,8 @@ <string name="mark_all_known_as_read">Marki ĉiujn Konatajn legitaj</string> <string name="mark_all_new_as_read">Marki ĉiujn Novajn legitaj</string> <string name="mark_all_as_read">Marki ĉiujn legitajn</string> - <string name="account_backup_tips2_md"> - ## Konsiletoj pri Ŝlosila Sekurkopiao kaj Sekureco - \n\nVia konto estas sekurigita per sekreta ŝlosilo. La ŝlosilo estas longa sinsekvo de signoj komencanta per **nsec1**. Ĉiu kiu havas aliron al ĉi tiu sekreta ŝlosilo povas afiŝi kaj ŝanĝi vian identecon. - \n\n- **Ne** metu vian sekretan ŝlosilon en ajnan retejon aŭ programaron, kiun vi ne fidas. - \n- Amethyst-programistoj **neniam** petos vian sekretan ŝlosilon. - \n- **Faru** sekuran sekurkopion de via sekreta ŝlosilo por reakiro de konto. Ni rekomendas uzi pasvortan administrilon. - </string> - <string name="account_backup_tips3_md"> - Por plia sekureco, vi povas ĉifri vian ŝlosilon per pasvorto. Ĉi tiu ŝlosilo komenciĝas per **ncryptsec1** kaj ne povas esti uzata sen via pasvorto. - \n\nSe vi perdas vian pasvorton, vi ne povos reakiri vian ŝlosilon. - </string> + <string name="account_backup_tips2_md">## Konsiletoj pri Ŝlosila Sekurkopiao kaj Sekureco \n\nVia konto estas sekurigita per sekreta ŝlosilo. La ŝlosilo estas longa sinsekvo de signoj komencanta per **nsec1**. Ĉiu kiu havas aliron al ĉi tiu sekreta ŝlosilo povas afiŝi kaj ŝanĝi vian identecon. \n\n- **Ne** metu vian sekretan ŝlosilon en ajnan retejon aŭ programaron, kiun vi ne fidas. \n- Amethyst-programistoj **neniam** petos vian sekretan ŝlosilon. \n- **Faru** sekuran sekurkopion de via sekreta ŝlosilo por reakiro de konto. Ni rekomendas uzi pasvortan administrilon.</string> + <string name="account_backup_tips3_md">Por plia sekureco, vi povas ĉifri vian ŝlosilon per pasvorto. Ĉi tiu ŝlosilo komenciĝas per **ncryptsec1** kaj ne povas esti uzata sen via pasvorto. \n\nSe vi perdas vian pasvorton, vi ne povos reakiri vian ŝlosilon.</string> <string name="encrypt_and_copy_my_secret_key">Ĉifri kaj kopii mian seketan ŝlosilon</string> <string name="badge_award_image_for">Ŝildeto-premia bildo por %1$s</string> <string name="badge_award_image">"Insigna premibildeto</string> @@ -962,10 +953,7 @@ <string name="push_server_none_explainer">Malaktivigi Puŝajn Sciigojn</string> <string name="push_server_uses_app_explainer">Uzas aplikaĵon %1$s</string> <string name="push_server_install_app">Puŝa Sciig-Agordo</string> - <string name="push_server_install_app_description"> - Por ricevi puŝajn sciigojn, instalu ajnan aplikaĵon kiu subtenas [Unified Push](https://unifiedpush.org/), kiel ekzemple [Ntfy](https://ntfy.sh/). - Post instalado, elektu la aplikaĵon, kiun vi volas uzi en la Agordoj. - </string> + <string name="push_server_install_app_description">Por ricevi puŝajn sciigojn, instalu ajnan aplikaĵon kiu subtenas [Unified Push](https://unifiedpush.org/), kiel ekzemple [Ntfy](https://ntfy.sh/). Post instalado, elektu la aplikaĵon, kiun vi volas uzi en la Agordoj.</string> <string name="thread_title">Fadeno</string> <string name="send_the_seller_a_message">Sendi mesaĝon al la vendisto</string> <string name="hi_there_is_this_still_available">Saluton, ĉu ĉi tio ankoraŭ disponeblas?</string> @@ -2058,15 +2046,7 @@ <string name="could_not_fetch_invoice_from_details">Ne eblis preni fakturon de %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Ne eblis malfermi la kunpremitan dosieron</string> <string name="couldnt_find_nwc_wallets">Monujo ne trovita</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst ne povis trovi ajnajn monujo-aplikojn, kiuj subtenas Nostr Wallet Connect (NWC). -\n\nKion vi povas fari: -\n\t1. Kontrolu NWC-subtenon: Duoble kontrolu, ĉu via preferata monujo-aplikaĵo estas kongrua kun Nostr Wallet Connect. -\n\t2. Krei Konekton: Iru al via monujo-aplikaĵo kaj kreu novan NWC-konekton. -\n\t3. Ligi al Amethyst: Via monujo kutime donos al vi tri opciojn: -\n\t\t3.1 Aŭtomata Ligo: Butono por aŭtomate malfermi kaj konekti al Amethyst. Se jes, alklaku tiun butonon, malfermu Amethyst kaj alklaku konservi sur la nova ekrano. -\n\t\t3.2 Mana Ligo: Konekta URI, kiun vi povas kopii kaj alklaki la algluu-ikonon sur ĉi tiu ekrano por rekte enigi ĝin. -\n\t\t3.3 QR-Kodo: La monujo kreas QR-kodon, kiun vi povas legi per la QR-koda butono sur ĉi tiu ekrano. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst ne povis trovi ajnajn monujo-aplikojn, kiuj subtenas Nostr Wallet Connect (NWC). \n\nKion vi povas fari: \n\t1. Kontrolu NWC-subtenon: Duoble kontrolu, ĉu via preferata monujo-aplikaĵo estas kongrua kun Nostr Wallet Connect. \n\t2. Krei Konekton: Iru al via monujo-aplikaĵo kaj kreu novan NWC-konekton. \n\t3. Ligi al Amethyst: Via monujo kutime donos al vi tri opciojn: \n\t\t3.1 Aŭtomata Ligo: Butono por aŭtomate malfermi kaj konekti al Amethyst. Se jes, alklaku tiun butonon, malfermu Amethyst kaj alklaku konservi sur la nova ekrano. \n\t\t3.2 Mana Ligo: Konekta URI, kiun vi povas kopii kaj alklaki la algluu-ikonon sur ĉi tiu ekrano por rekte enigi ĝin. \n\t\t3.3 QR-Kodo: La monujo kreas QR-kodon, kiun vi povas legi per la QR-koda butono sur ĉi tiu ekrano.</string> <string name="create">Krei</string> <string name="d">t</string> <string name="danger_zone">Danĝera Zono</string> @@ -2754,11 +2734,8 @@ <string name="proxy_section">Prokuro-Relay-oj</string> <string name="proxy_section_explainer">Agregataj relay-oj, kiujn la aplikaĵo devas uzi por elŝuti viajn fluojn, ekzemple filter.nostr.wine. Ĉi tio anstataŭas la sendkesta modelon kaj igos la aplikaĵon konektiĝi nur al la relay-oj en viaj listoj.</string> <string name="public_chat">Publika Babilado</string> - <string name="public_chat_explainer">Publikaj babilejoj estas videblaj al ĉiuj en Nostr kaj ĉiu ajn - povas partopreni en ili. Ili estas bonegaj por malfermitaj komunumoj pri specifaj temoj. - Moderado povas esti kontrolata per forigo de afiŝoj sur siaj relays</string> - <string name="public_chat_relays_explainer">Enigu inter 1–3 relays, kiuj gastigas ĉi tiun grupon. - Nostr-klientoj uzas ĉi tiun agordon por scii, kie elŝuti mesaĝojn kaj kien sendi viajn mesaĝojn.</string> + <string name="public_chat_explainer">Publikaj babilejoj estas videblaj al ĉiuj en Nostr kaj ĉiu ajn povas partopreni en ili. Ili estas bonegaj por malfermitaj komunumoj pri specifaj temoj. Moderado povas esti kontrolata per forigo de afiŝoj sur siaj relays</string> + <string name="public_chat_relays_explainer">Enigu inter 1–3 relays, kiuj gastigas ĉi tiun grupon. Nostr-klientoj uzas ĉi tiun agordon por scii, kie elŝuti mesaĝojn kaj kien sendi viajn mesaĝojn.</string> <string name="public_chat_relays_title">Relay-oj</string> <string name="public_chat_title">Publikaj Babilejaj Metadatenoj</string> <string name="public_chats">Publikaj Babilejoj</string> @@ -2783,10 +2760,7 @@ <string name="read_only_user">Nur-lega uzanto</string> <string name="record_a_message_description">Alklaku kaj tenu por registri mesaĝon</string> <string name="relay_chat">Relay-Babilejo</string> - <string name="relay_chat_explainer">Relay-babilejoj estas babilaj grupoj kontrolataj de sia hejma relay. - Ili estas videblaj al ĉiuj en Nostr kaj ĉiu ajn povas partopreni en ili. - Ili estas bonegaj por malfermitaj komunumoj pri specifaj temoj. Kelkaj el ĉi tiuj grupoj estas efemeraj - kaj tial babilmesaĝoj malaperas kun la tempo</string> + <string name="relay_chat_explainer">Relay-babilejoj estas babilaj grupoj kontrolataj de sia hejma relay. Ili estas videblaj al ĉiuj en Nostr kaj ĉiu ajn povas partopreni en ili. Ili estas bonegaj por malfermitaj komunumoj pri specifaj temoj. Kelkaj el ĉi tiuj grupoj estas efemeraj kaj tial babilmesaĝoj malaperas kun la tempo</string> <string name="relay_chat_title">Relay-Babilejaroj</string> <string name="relay_management_tab_events">Eventoj</string> <string name="relay_management_tab_ips">IP-oj</string> diff --git a/commonsUI/src/commonMain/composeResources/values-es-rES/strings.xml b/commonsUI/src/commonMain/composeResources/values-es-rES/strings.xml index ee9c2f0a5b..45b069c10c 100644 --- a/commonsUI/src/commonMain/composeResources/values-es-rES/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-es-rES/strings.xml @@ -224,7 +224,7 @@ <string name="copy_user_id">Copiar ID de usuario</string> <string name="unblock_user">Desbloquear usuario</string> <string name="user_is_blocked_hidden">Este usuario está bloqueado/silenciado. Sus publicaciones están ocultas.</string> - <string name="npub_hex_username">npub, hex, username </string> + <string name="npub_hex_username">npub, hex, username</string> <string name="clear">Limpiar</string> <string name="app_logo">Logo de la app</string> <string name="nsec_npub_hex_private_key">nsec o npub</string> @@ -272,15 +272,8 @@ <string name="mark_all_known_as_read">Marcar los conocidos como leídos</string> <string name="mark_all_new_as_read">Marcar los nuevos como leídos</string> <string name="mark_all_as_read">Marcar todos como leídos</string> - <string name="account_backup_tips2_md"> ## Consejos sobre la protección y la copia de seguridad de las claves - \n\nTu cuenta está protegida por una clave secreta. La clave es una larga secuencia de caracteres que empieza por **nsec1**. Cualquiera que tenga acceso a esta clave secreta puede publicar y cambiar tu identidad. - \n\n- **No** guardes la clave secreta en ningún sitio web o software en los que no confíes. - \n- Los desarrolladores de Amethyst **nunca** te pedirán la clave secreta. - \n- **Sí** puedes realizar una copia de seguridad de la clave secreta para recuperar la cuenta. Para ello, te recomendamos usar un gestor de contraseñas. - </string> - <string name="account_backup_tips3_md"> Para mayor seguridad, puedes cifrar la clave con una contraseña. Esta clave empieza por **ncryptsec1** y no puede utilizarse sin tu contraseña. - \n\nSi pierdes la contraseña, no podrás recuperar tu clave. - </string> + <string name="account_backup_tips2_md">## Consejos sobre la protección y la copia de seguridad de las claves \n\nTu cuenta está protegida por una clave secreta. La clave es una larga secuencia de caracteres que empieza por **nsec1**. Cualquiera que tenga acceso a esta clave secreta puede publicar y cambiar tu identidad. \n\n- **No** guardes la clave secreta en ningún sitio web o software en los que no confíes. \n- Los desarrolladores de Amethyst **nunca** te pedirán la clave secreta. \n- **Sí** puedes realizar una copia de seguridad de la clave secreta para recuperar la cuenta. Para ello, te recomendamos usar un gestor de contraseñas.</string> + <string name="account_backup_tips3_md">Para mayor seguridad, puedes cifrar la clave con una contraseña. Esta clave empieza por **ncryptsec1** y no puede utilizarse sin tu contraseña. \n\nSi pierdes la contraseña, no podrás recuperar tu clave.</string> <string name="encrypt_and_copy_my_secret_key">Cifrar y copiar mi clave secreta</string> <string name="badge_award_image_for">Imagen del Badge para %1$s</string> <string name="badge_award_image">"Imagen de premio de insignia</string> @@ -962,9 +955,7 @@ <string name="push_server_none_explainer">Desactiva las notificaciones push</string> <string name="push_server_uses_app_explainer">Usa la app %1$s</string> <string name="push_server_install_app">Configuración de notificaciones push</string> - <string name="push_server_install_app_description"> Para recibir notificaciones push, instala cualquier aplicación compatible con [Unified Push](https://unifiedpush.org/), como [Ntfy](https://ntfy.sh/). - Tras la instalación, selecciona la aplicación que deseas usar en la configuración. - </string> + <string name="push_server_install_app_description">Para recibir notificaciones push, instala cualquier aplicación compatible con [Unified Push](https://unifiedpush.org/), como [Ntfy](https://ntfy.sh/). Tras la instalación, selecciona la aplicación que deseas usar en la configuración.</string> <string name="thread_title">Conversación</string> <string name="send_the_seller_a_message">Envía un mensaje al vendedor</string> <string name="hi_there_is_this_still_available">Hola, ¿esto todavía está disponible?</string> @@ -2028,15 +2019,7 @@ <string name="could_not_fetch_invoice_from_details">No se ha podido obtener la factura de %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">No se pudo abrir el archivo comprimido</string> <string name="couldnt_find_nwc_wallets">Cartera no encontrada</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst no encontró ninguna aplicación de cartera compatible con Nostr Wallet Connect (NWC). -\n\nQué puedes hacer: -\n\t1. Verificar la compatibilidad con NWC: Comprueba que tu aplicación de cartera preferida es compatible con Nostr Wallet Connect. -\n\t2. Crear conexión: Ve a tu aplicación de cartera y crea una nueva conexión NWC. -\n\t3. Vincular con Amethyst: Tu cartera normalmente te ofrecerá tres opciones: -\n\t\t3.1 Enlace automático: Un botón para abrir y conectar automáticamente a Amethyst. Si es así, pulsa ese botón, abre Amethyst y guarda en la nueva pantalla. -\n\t\t3.2 Enlace manual: Un URI de conexión que puedes copiar y pegar usando el icono de pegado en esta pantalla para introducirlo directamente. -\n\t\t3.3 Código QR: La cartera genera un código QR que puedes leer con el botón de código QR en esta pantalla. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst no encontró ninguna aplicación de cartera compatible con Nostr Wallet Connect (NWC). \n\nQué puedes hacer: \n\t1. Verificar la compatibilidad con NWC: Comprueba que tu aplicación de cartera preferida es compatible con Nostr Wallet Connect. \n\t2. Crear conexión: Ve a tu aplicación de cartera y crea una nueva conexión NWC. \n\t3. Vincular con Amethyst: Tu cartera normalmente te ofrecerá tres opciones: \n\t\t3.1 Enlace automático: Un botón para abrir y conectar automáticamente a Amethyst. Si es así, pulsa ese botón, abre Amethyst y guarda en la nueva pantalla. \n\t\t3.2 Enlace manual: Un URI de conexión que puedes copiar y pegar usando el icono de pegado en esta pantalla para introducirlo directamente. \n\t\t3.3 Código QR: La cartera genera un código QR que puedes leer con el botón de código QR en esta pantalla.</string> <string name="create">Crear</string> <string name="d">d</string> <string name="danger_zone">Zona peligrosa</string> @@ -2715,11 +2698,8 @@ <string name="proxy_section">Relés de proxy</string> <string name="proxy_section_explainer">Relés recopiladores que la app debe usar para descargar tus fuentes, como filter.nostr.wine. Esto reemplaza el modelo de bandeja de salida y hará que la app se conecte solo a los relés de tus listas.</string> <string name="public_chat">Chat público</string> - <string name="public_chat_explainer">Los chats públicos son visibles para todos en Nostr y cualquiera - puede participar en ellos. Son ideales para comunidades abiertas alrededor de temas específicos. - La moderación puede controlarse eliminando publicaciones en sus relés.</string> - <string name="public_chat_relays_explainer">Inserta entre 1 y 3 relés que alojan este grupo. - Los clientes de Nostr utilizan esta configuración para saber de dónde descargar y adónde enviar los mensajes.</string> + <string name="public_chat_explainer">Los chats públicos son visibles para todos en Nostr y cualquiera puede participar en ellos. Son ideales para comunidades abiertas alrededor de temas específicos. La moderación puede controlarse eliminando publicaciones en sus relés.</string> + <string name="public_chat_relays_explainer">Inserta entre 1 y 3 relés que alojan este grupo. Los clientes de Nostr utilizan esta configuración para saber de dónde descargar y adónde enviar los mensajes.</string> <string name="public_chat_relays_title">Relés</string> <string name="public_chat_title">Metadatos públicos del chat</string> <string name="public_chats">Chats públicos</string> @@ -2744,10 +2724,7 @@ <string name="read_only_user">Usuario de solo lectura</string> <string name="record_a_message_description">Haz clic y mantén pulsado para grabar un mensaje</string> <string name="relay_chat">Chat en relé</string> - <string name="relay_chat_explainer">Los chats en relé son grupos de chat controlados por su relé de origen. - Son visibles para todos los usuarios de Nostr y cualquiera puede participar en ellos. - Son ideales para comunidades abiertas en torno a temas específicos. Algunos de estos grupos son efímeros - y, por lo tanto, los mensajes del chat desaparecen con el tiempo</string> + <string name="relay_chat_explainer">Los chats en relé son grupos de chat controlados por su relé de origen. Son visibles para todos los usuarios de Nostr y cualquiera puede participar en ellos. Son ideales para comunidades abiertas en torno a temas específicos. Algunos de estos grupos son efímeros y, por lo tanto, los mensajes del chat desaparecen con el tiempo</string> <string name="relay_chat_title">Chats en relé</string> <string name="relay_management_tab_events">Eventos</string> <string name="relay_management_tab_kinds">Tipos</string> diff --git a/commonsUI/src/commonMain/composeResources/values-es-rMX/strings.xml b/commonsUI/src/commonMain/composeResources/values-es-rMX/strings.xml index f3e6fa1ca8..d941f9ea99 100644 --- a/commonsUI/src/commonMain/composeResources/values-es-rMX/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-es-rMX/strings.xml @@ -272,15 +272,8 @@ <string name="mark_all_known_as_read">Marcar todos los conocidos como leídos</string> <string name="mark_all_new_as_read">Marcar todos los nuevos como leídos</string> <string name="mark_all_as_read">Marcar todos como leídos</string> - <string name="account_backup_tips2_md"> ## Consejos sobre la protección y la copia de seguridad de las claves - \n\nTu cuenta está protegida por una clave secreta. La clave es una larga secuencia de caracteres que empieza por **nsec1**. Cualquiera que tenga acceso a esta clave secreta puede publicar y cambiar tu identidad. - \n\n- **No** guardes la clave secreta en ningún sitio web o software en los que no confíes. - \n- Los desarrolladores de Amethyst **nunca** te pedirán la clave secreta. - \n- **Sí** puedes realizar una copia de seguridad de la clave secreta para recuperar la cuenta. Para ello, te recomendamos usar un gestor de contraseñas. - </string> - <string name="account_backup_tips3_md"> Para mayor seguridad, puedes cifrar la clave con una contraseña. Esta clave empieza por **ncryptsec1** y no puede utilizarse sin tu contraseña. - \n\nSi pierdes la contraseña, no podrás recuperar tu clave. - </string> + <string name="account_backup_tips2_md">## Consejos sobre la protección y la copia de seguridad de las claves \n\nTu cuenta está protegida por una clave secreta. La clave es una larga secuencia de caracteres que empieza por **nsec1**. Cualquiera que tenga acceso a esta clave secreta puede publicar y cambiar tu identidad. \n\n- **No** guardes la clave secreta en ningún sitio web o software en los que no confíes. \n- Los desarrolladores de Amethyst **nunca** te pedirán la clave secreta. \n- **Sí** puedes realizar una copia de seguridad de la clave secreta para recuperar la cuenta. Para ello, te recomendamos usar un gestor de contraseñas.</string> + <string name="account_backup_tips3_md">Para mayor seguridad, puedes cifrar la clave con una contraseña. Esta clave empieza por **ncryptsec1** y no puede utilizarse sin tu contraseña. \n\nSi pierdes la contraseña, no podrás recuperar tu clave.</string> <string name="encrypt_and_copy_my_secret_key">Cifrar y copiar mi clave secreta</string> <string name="badge_award_image_for">Imagen de premio de insignia por %1$s</string> <string name="badge_award_image">"Imagen de premio de insignia</string> @@ -961,9 +954,7 @@ <string name="push_server_none_explainer">Desactiva las notificaciones push</string> <string name="push_server_uses_app_explainer">Usa la app %1$s</string> <string name="push_server_install_app">Configuración de notificaciones push</string> - <string name="push_server_install_app_description"> Para recibir notificaciones push, instala cualquier aplicación compatible con [Unified Push](https://unifiedpush.org/), como [Ntfy](https://ntfy.sh/). - Tras la instalación, selecciona la aplicación que deseas usar en la configuración. - </string> + <string name="push_server_install_app_description">Para recibir notificaciones push, instala cualquier aplicación compatible con [Unified Push](https://unifiedpush.org/), como [Ntfy](https://ntfy.sh/). Tras la instalación, selecciona la aplicación que deseas usar en la configuración.</string> <string name="thread_title">Hilo</string> <string name="send_the_seller_a_message">Envía un mensaje al vendedor</string> <string name="hi_there_is_this_still_available">Hola, ¿esto todavía está disponible?</string> @@ -2027,15 +2018,7 @@ <string name="could_not_fetch_invoice_from_details">No se pudo obtener la factura de %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">No se pudo abrir el archivo comprimido</string> <string name="couldnt_find_nwc_wallets">Billetera no encontrada</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst no encontró ninguna aplicación de cartera compatible con Nostr Wallet Connect (NWC). -\n\nQué puedes hacer: -\n\t1. Verificar la compatibilidad con NWC: Comprueba que tu aplicación de cartera preferida es compatible con Nostr Wallet Connect. -\n\t2. Crear conexión: Ve a tu aplicación de cartera y crea una nueva conexión NWC. -\n\t3. Vincular con Amethyst: Tu cartera normalmente te ofrecerá tres opciones: -\n\t\t3.1 Enlace automático: Un botón para abrir y conectar automáticamente a Amethyst. Si es así, pulsa ese botón, abre Amethyst y guarda en la nueva pantalla. -\n\t\t3.2 Enlace manual: Un URI de conexión que puedes copiar y pegar usando el icono de pegado en esta pantalla para introducirlo directamente. -\n\t\t3.3 Código QR: La cartera genera un código QR que puedes leer con el botón de código QR en esta pantalla. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst no encontró ninguna aplicación de cartera compatible con Nostr Wallet Connect (NWC). \n\nQué puedes hacer: \n\t1. Verificar la compatibilidad con NWC: Comprueba que tu aplicación de cartera preferida es compatible con Nostr Wallet Connect. \n\t2. Crear conexión: Ve a tu aplicación de cartera y crea una nueva conexión NWC. \n\t3. Vincular con Amethyst: Tu cartera normalmente te ofrecerá tres opciones: \n\t\t3.1 Enlace automático: Un botón para abrir y conectar automáticamente a Amethyst. Si es así, pulsa ese botón, abre Amethyst y guarda en la nueva pantalla. \n\t\t3.2 Enlace manual: Un URI de conexión que puedes copiar y pegar usando el icono de pegado en esta pantalla para introducirlo directamente. \n\t\t3.3 Código QR: La cartera genera un código QR que puedes leer con el botón de código QR en esta pantalla.</string> <string name="create">Crear</string> <string name="d">d</string> <string name="danger_zone">Zona peligrosa</string> @@ -2708,11 +2691,8 @@ <string name="proxy_section">Relés de proxy</string> <string name="proxy_section_explainer">Relés recopiladores que la app debe usar para descargar tus feeds, como filter.nostr.wine. Esto reemplaza el modelo de bandeja de salida y hará que la app se conecte solo a los relés de tus listas.</string> <string name="public_chat">Chat público</string> - <string name="public_chat_explainer">Los chats públicos son visibles para todos en Nostr y cualquiera - puede participar en ellos. Son ideales para comunidades abiertas alrededor de temas específicos. - La moderación puede controlarse eliminando publicaciones en sus relés.</string> - <string name="public_chat_relays_explainer">Inserta entre 1 y 3 relés que alojan este grupo. - Los clientes de Nostr utilizan esta configuración para saber de dónde descargar y adónde enviar los mensajes.</string> + <string name="public_chat_explainer">Los chats públicos son visibles para todos en Nostr y cualquiera puede participar en ellos. Son ideales para comunidades abiertas alrededor de temas específicos. La moderación puede controlarse eliminando publicaciones en sus relés.</string> + <string name="public_chat_relays_explainer">Inserta entre 1 y 3 relés que alojan este grupo. Los clientes de Nostr utilizan esta configuración para saber de dónde descargar y adónde enviar los mensajes.</string> <string name="public_chat_relays_title">Relés</string> <string name="public_chat_title">Metadatos públicos del chat</string> <string name="public_chats">Chats públicos</string> @@ -2737,10 +2717,7 @@ <string name="read_only_user">Usuario de solo lectura</string> <string name="record_a_message_description">Haz clic y mantén presionado para grabar un mensaje</string> <string name="relay_chat">Chat en relé</string> - <string name="relay_chat_explainer">Los chats en relé son grupos de chat controlados por su relé de origen. - Son visibles para todos los usuarios de Nostr y cualquiera puede participar en ellos. - Son ideales para comunidades abiertas en torno a temas específicos. Algunos de estos grupos son efímeros - y, por lo tanto, los mensajes del chat desaparecen con el tiempo</string> + <string name="relay_chat_explainer">Los chats en relé son grupos de chat controlados por su relé de origen. Son visibles para todos los usuarios de Nostr y cualquiera puede participar en ellos. Son ideales para comunidades abiertas en torno a temas específicos. Algunos de estos grupos son efímeros y, por lo tanto, los mensajes del chat desaparecen con el tiempo</string> <string name="relay_chat_title">Chats en relé</string> <string name="relay_management_tab_events">Eventos</string> <string name="relay_management_tab_kinds">Tipos</string> diff --git a/commonsUI/src/commonMain/composeResources/values-es-rUS/strings.xml b/commonsUI/src/commonMain/composeResources/values-es-rUS/strings.xml index a86f5c2aaf..3e6060ab9c 100644 --- a/commonsUI/src/commonMain/composeResources/values-es-rUS/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-es-rUS/strings.xml @@ -272,15 +272,8 @@ <string name="mark_all_known_as_read">Marcar todos los conocidos como leídos</string> <string name="mark_all_new_as_read">Marcar todos los nuevos como leídos</string> <string name="mark_all_as_read">Marcar todos como leídos</string> - <string name="account_backup_tips2_md"> ## Consejos sobre la protección y la copia de seguridad de las claves - \n\nTu cuenta está protegida por una clave secreta. La clave es una larga secuencia de caracteres que empieza por **nsec1**. Cualquiera que tenga acceso a esta clave secreta puede publicar y cambiar tu identidad. - \n\n- **No** guardes la clave secreta en ningún sitio web o software en los que no confíes. - \n- Los desarrolladores de Amethyst **nunca** te pedirán la clave secreta. - \n- **Sí** puedes realizar una copia de seguridad de la clave secreta para recuperar la cuenta. Para ello, te recomendamos usar un gestor de contraseñas. - </string> - <string name="account_backup_tips3_md"> Para mayor seguridad, puedes cifrar la clave con una contraseña. Esta clave empieza por **ncryptsec1** y no puede utilizarse sin tu contraseña. - \n\nSi pierdes la contraseña, no podrás recuperar tu clave. - </string> + <string name="account_backup_tips2_md">## Consejos sobre la protección y la copia de seguridad de las claves \n\nTu cuenta está protegida por una clave secreta. La clave es una larga secuencia de caracteres que empieza por **nsec1**. Cualquiera que tenga acceso a esta clave secreta puede publicar y cambiar tu identidad. \n\n- **No** guardes la clave secreta en ningún sitio web o software en los que no confíes. \n- Los desarrolladores de Amethyst **nunca** te pedirán la clave secreta. \n- **Sí** puedes realizar una copia de seguridad de la clave secreta para recuperar la cuenta. Para ello, te recomendamos usar un gestor de contraseñas.</string> + <string name="account_backup_tips3_md">Para mayor seguridad, puedes cifrar la clave con una contraseña. Esta clave empieza por **ncryptsec1** y no puede utilizarse sin tu contraseña. \n\nSi pierdes la contraseña, no podrás recuperar tu clave.</string> <string name="encrypt_and_copy_my_secret_key">Cifrar y copiar mi clave secreta</string> <string name="badge_award_image_for">Imagen de premio de insignia por %1$s</string> <string name="badge_award_image">"Imagen de premio de insignia</string> @@ -961,9 +954,7 @@ <string name="push_server_none_explainer">Desactiva las notificaciones push</string> <string name="push_server_uses_app_explainer">Usa la app %1$s</string> <string name="push_server_install_app">Configuración de notificaciones push</string> - <string name="push_server_install_app_description"> Para recibir notificaciones push, instala cualquier aplicación compatible con [Unified Push](https://unifiedpush.org/), como [Ntfy](https://ntfy.sh/). - Tras la instalación, selecciona la aplicación que deseas usar en la configuración. - </string> + <string name="push_server_install_app_description">Para recibir notificaciones push, instala cualquier aplicación compatible con [Unified Push](https://unifiedpush.org/), como [Ntfy](https://ntfy.sh/). Tras la instalación, selecciona la aplicación que deseas usar en la configuración.</string> <string name="thread_title">Hilo</string> <string name="send_the_seller_a_message">Envía un mensaje al vendedor</string> <string name="hi_there_is_this_still_available">Hola, ¿esto todavía está disponible?</string> @@ -2027,15 +2018,7 @@ <string name="could_not_fetch_invoice_from_details">No se pudo obtener la factura de %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">No se pudo abrir el archivo comprimido</string> <string name="couldnt_find_nwc_wallets">Billetera no encontrada</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst no encontró ninguna aplicación de cartera compatible con Nostr Wallet Connect (NWC). -\n\nQué puedes hacer: -\n\t1. Verificar la compatibilidad con NWC: Comprueba que tu aplicación de cartera preferida es compatible con Nostr Wallet Connect. -\n\t2. Crear conexión: Ve a tu aplicación de cartera y crea una nueva conexión NWC. -\n\t3. Vincular con Amethyst: Tu cartera normalmente te ofrecerá tres opciones: -\n\t\t3.1 Enlace automático: Un botón para abrir y conectar automáticamente a Amethyst. Si es así, pulsa ese botón, abre Amethyst y guarda en la nueva pantalla. -\n\t\t3.2 Enlace manual: Un URI de conexión que puedes copiar y pegar usando el icono de pegado en esta pantalla para introducirlo directamente. -\n\t\t3.3 Código QR: La cartera genera un código QR que puedes leer con el botón de código QR en esta pantalla. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst no encontró ninguna aplicación de cartera compatible con Nostr Wallet Connect (NWC). \n\nQué puedes hacer: \n\t1. Verificar la compatibilidad con NWC: Comprueba que tu aplicación de cartera preferida es compatible con Nostr Wallet Connect. \n\t2. Crear conexión: Ve a tu aplicación de cartera y crea una nueva conexión NWC. \n\t3. Vincular con Amethyst: Tu cartera normalmente te ofrecerá tres opciones: \n\t\t3.1 Enlace automático: Un botón para abrir y conectar automáticamente a Amethyst. Si es así, pulsa ese botón, abre Amethyst y guarda en la nueva pantalla. \n\t\t3.2 Enlace manual: Un URI de conexión que puedes copiar y pegar usando el icono de pegado en esta pantalla para introducirlo directamente. \n\t\t3.3 Código QR: La cartera genera un código QR que puedes leer con el botón de código QR en esta pantalla.</string> <string name="create">Crear</string> <string name="d">d</string> <string name="danger_zone">Zona peligrosa</string> @@ -2708,11 +2691,8 @@ <string name="proxy_section">Relés de proxy</string> <string name="proxy_section_explainer">Relés recopiladores que la app debe usar para descargar tus feeds, como filter.nostr.wine. Esto reemplaza el modelo de bandeja de salida y hará que la app se conecte solo a los relés de tus listas.</string> <string name="public_chat">Chat público</string> - <string name="public_chat_explainer">Los chats públicos son visibles para todos en Nostr y cualquiera - puede participar en ellos. Son ideales para comunidades abiertas alrededor de temas específicos. - La moderación puede controlarse eliminando mensajes en sus relés.</string> - <string name="public_chat_relays_explainer">Inserta entre 1 y 3 relés que alojan este grupo. - Los clientes de Nostr utilizan esta configuración para saber de dónde descargar y adónde enviar los mensajes.</string> + <string name="public_chat_explainer">Los chats públicos son visibles para todos en Nostr y cualquiera puede participar en ellos. Son ideales para comunidades abiertas alrededor de temas específicos. La moderación puede controlarse eliminando mensajes en sus relés.</string> + <string name="public_chat_relays_explainer">Inserta entre 1 y 3 relés que alojan este grupo. Los clientes de Nostr utilizan esta configuración para saber de dónde descargar y adónde enviar los mensajes.</string> <string name="public_chat_relays_title">Relés</string> <string name="public_chat_title">Metadatos públicos del chat</string> <string name="public_chats">Chats públicos</string> @@ -2737,10 +2717,7 @@ <string name="read_only_user">Usuario de solo lectura</string> <string name="record_a_message_description">Haz clic y mantén presionado para grabar un mensaje</string> <string name="relay_chat">Chat en relé</string> - <string name="relay_chat_explainer">Los chats en relé son grupos de chat controlados por su relé de origen. - Son visibles para todos los usuarios de Nostr y cualquiera puede participar en ellos. - Son ideales para comunidades abiertas en torno a temas específicos. Algunos de estos grupos son efímeros - y, por lo tanto, los mensajes del chat desaparecen con el tiempo</string> + <string name="relay_chat_explainer">Los chats en relé son grupos de chat controlados por su relé de origen. Son visibles para todos los usuarios de Nostr y cualquiera puede participar en ellos. Son ideales para comunidades abiertas en torno a temas específicos. Algunos de estos grupos son efímeros y, por lo tanto, los mensajes del chat desaparecen con el tiempo</string> <string name="relay_chat_title">Chats en relé</string> <string name="relay_management_tab_events">Eventos</string> <string name="relay_management_tab_kinds">Tipos</string> diff --git a/commonsUI/src/commonMain/composeResources/values-es/strings.xml b/commonsUI/src/commonMain/composeResources/values-es/strings.xml index d9570812e6..0f52873364 100644 --- a/commonsUI/src/commonMain/composeResources/values-es/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-es/strings.xml @@ -211,7 +211,7 @@ <string name="copy_user_id">Copiar ID de usuario</string> <string name="unblock_user">Desbloquear usuario</string> <string name="user_is_blocked_hidden">Este usuario está bloqueado/silenciado. Sus publicaciones están ocultas.</string> - <string name="npub_hex_username">npub, hex, username </string> + <string name="npub_hex_username">npub, hex, username</string> <string name="clear">Limpiar</string> <string name="app_logo">Logo de la app</string> <string name="nsec_npub_hex_private_key">nsec / npub / hex private key</string> @@ -951,9 +951,7 @@ <string name="push_server_none_explainer">Desactiva las notificaciones push</string> <string name="push_server_uses_app_explainer">Usa la app %1$s</string> <string name="push_server_install_app">Configuración de notificaciones push</string> - <string name="push_server_install_app_description"> Para recibir notificaciones push, instala cualquier aplicación compatible con [Unified Push](https://unifiedpush.org/), como [Ntfy](https://ntfy.sh/). - Tras la instalación, selecciona la aplicación que deseas usar en la configuración. - </string> + <string name="push_server_install_app_description">Para recibir notificaciones push, instala cualquier aplicación compatible con [Unified Push](https://unifiedpush.org/), como [Ntfy](https://ntfy.sh/). Tras la instalación, selecciona la aplicación que deseas usar en la configuración.</string> <string name="thread_title">Conversación</string> <string name="send_the_seller_a_message">Envía un mensaje al vendedor</string> <string name="hi_there_is_this_still_available">Hola, ¿esto todavía está disponible?</string> @@ -1802,15 +1800,8 @@ <string name="ots_explorer_api_label">URL base de la API del explorador</string> <string name="dvm_pay_invoice_from_dvm">Pagar factura del DVM</string> <string name="dvm_pay_amount_to_dvm">Pagar %1$s sats al DVM</string> - <string xmlns:ns0="http://schemas.android.com/tools" name="account_backup_tips2_md" ns0:ignore="Typos"> ## Consejos sobre la protección y la copia de seguridad de las claves - \n\nTu cuenta está protegida por una clave secreta. La clave es una larga secuencia de caracteres que empieza por **nsec1**. Cualquiera que tenga acceso a esta clave secreta puede publicar y cambiar tu identidad. - \n\n- **No** guardes la clave secreta en ningún sitio web o software en los que no confíes. - \n- Los desarrolladores de Amethyst **nunca** te pedirán la clave secreta. - \n- **Sí** puedes realizar una copia de seguridad de la clave secreta para recuperar la cuenta. Para ello, te recomendamos usar un gestor de contraseñas. - </string> - <string xmlns:ns0="http://schemas.android.com/tools" name="account_backup_tips3_md" ns0:ignore="Typos"> Para mayor seguridad, puedes cifrar la clave con una contraseña. Esta clave empieza por **ncryptsec1** y no puede utilizarse sin tu contraseña. - \n\nSi pierdes la contraseña, no podrás recuperar tu clave. - </string> + <string xmlns:ns0="http://schemas.android.com/tools" name="account_backup_tips2_md" ns0:ignore="Typos">## Consejos sobre la protección y la copia de seguridad de las claves \n\nTu cuenta está protegida por una clave secreta. La clave es una larga secuencia de caracteres que empieza por **nsec1**. Cualquiera que tenga acceso a esta clave secreta puede publicar y cambiar tu identidad. \n\n- **No** guardes la clave secreta en ningún sitio web o software en los que no confíes. \n- Los desarrolladores de Amethyst **nunca** te pedirán la clave secreta. \n- **Sí** puedes realizar una copia de seguridad de la clave secreta para recuperar la cuenta. Para ello, te recomendamos usar un gestor de contraseñas.</string> + <string xmlns:ns0="http://schemas.android.com/tools" name="account_backup_tips3_md" ns0:ignore="Typos">Para mayor seguridad, puedes cifrar la clave con una contraseña. Esta clave empieza por **ncryptsec1** y no puede utilizarse sin tu contraseña. \n\nSi pierdes la contraseña, no podrás recuperar tu clave.</string> <string name="use_direct_url">Usar URL directa</string> <string name="nsite_title">nSite: %1$s</string> <string name="napplet_card_title">nApplet: %1$s</string> @@ -2055,15 +2046,7 @@ <string name="could_not_fetch_invoice_from_details">No se ha podido obtener la factura de %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">No se pudo abrir el archivo comprimido</string> <string name="couldnt_find_nwc_wallets">Cartera no encontrada</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst no encontró ninguna aplicación de cartera compatible con Nostr Wallet Connect (NWC). -\n\nQué puedes hacer: -\n\t1. Verificar la compatibilidad con NWC: Comprueba que tu aplicación de cartera preferida es compatible con Nostr Wallet Connect. -\n\t2. Crear conexión: Ve a tu aplicación de cartera y crea una nueva conexión NWC. -\n\t3. Vincular con Amethyst: Tu cartera normalmente te ofrecerá tres opciones: -\n\t\t3.1 Enlace automático: Un botón para abrir y conectar automáticamente a Amethyst. Si es así, pulsa ese botón, abre Amethyst y guarda en la nueva pantalla. -\n\t\t3.2 Enlace manual: Un URI de conexión que puedes copiar y pegar usando el icono de pegado en esta pantalla para introducirlo directamente. -\n\t\t3.3 Código QR: La cartera genera un código QR que puedes leer con el botón de código QR en esta pantalla. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst no encontró ninguna aplicación de cartera compatible con Nostr Wallet Connect (NWC). \n\nQué puedes hacer: \n\t1. Verificar la compatibilidad con NWC: Comprueba que tu aplicación de cartera preferida es compatible con Nostr Wallet Connect. \n\t2. Crear conexión: Ve a tu aplicación de cartera y crea una nueva conexión NWC. \n\t3. Vincular con Amethyst: Tu cartera normalmente te ofrecerá tres opciones: \n\t\t3.1 Enlace automático: Un botón para abrir y conectar automáticamente a Amethyst. Si es así, pulsa ese botón, abre Amethyst y guarda en la nueva pantalla. \n\t\t3.2 Enlace manual: Un URI de conexión que puedes copiar y pegar usando el icono de pegado en esta pantalla para introducirlo directamente. \n\t\t3.3 Código QR: La cartera genera un código QR que puedes leer con el botón de código QR en esta pantalla.</string> <string name="create">Crear</string> <string name="d">d</string> <string name="danger_zone">Zona peligrosa</string> @@ -2751,11 +2734,8 @@ <string name="proxy_section">Relés de proxy</string> <string name="proxy_section_explainer">Relés recopiladores que la app debe usar para descargar tus fuentes, como filter.nostr.wine. Esto reemplaza el modelo de bandeja de salida y hará que la app se conecte solo a los relés de tus listas.</string> <string name="public_chat">Chat público</string> - <string name="public_chat_explainer">Los chats públicos son visibles para todos en Nostr y cualquiera - puede participar en ellos. Son ideales para comunidades abiertas alrededor de temas específicos. - La moderación puede controlarse eliminando publicaciones en sus relés.</string> - <string name="public_chat_relays_explainer">Inserta entre 1 y 3 relés que alojan este grupo. - Los clientes de Nostr utilizan esta configuración para saber de dónde descargar y adónde enviar los mensajes.</string> + <string name="public_chat_explainer">Los chats públicos son visibles para todos en Nostr y cualquiera puede participar en ellos. Son ideales para comunidades abiertas alrededor de temas específicos. La moderación puede controlarse eliminando publicaciones en sus relés.</string> + <string name="public_chat_relays_explainer">Inserta entre 1 y 3 relés que alojan este grupo. Los clientes de Nostr utilizan esta configuración para saber de dónde descargar y adónde enviar los mensajes.</string> <string name="public_chat_relays_title">Relés</string> <string name="public_chat_title">Metadatos públicos del chat</string> <string name="public_chats">Chats públicos</string> @@ -2780,10 +2760,7 @@ <string name="read_only_user">Usuario de solo lectura</string> <string name="record_a_message_description">Haz clic y mantén pulsado para grabar un mensaje</string> <string name="relay_chat">Chat en relé</string> - <string name="relay_chat_explainer">Los chats en relé son grupos de chat controlados por su relé de origen. - Son visibles para todos los usuarios de Nostr y cualquiera puede participar en ellos. - Son ideales para comunidades abiertas en torno a temas específicos. Algunos de estos grupos son efímeros - y, por lo tanto, los mensajes del chat desaparecen con el tiempo</string> + <string name="relay_chat_explainer">Los chats en relé son grupos de chat controlados por su relé de origen. Son visibles para todos los usuarios de Nostr y cualquiera puede participar en ellos. Son ideales para comunidades abiertas en torno a temas específicos. Algunos de estos grupos son efímeros y, por lo tanto, los mensajes del chat desaparecen con el tiempo</string> <string name="relay_chat_title">Chats en relé</string> <string name="relay_management_tab_events">Eventos</string> <string name="relay_management_tab_ips">IPs</string> diff --git a/commonsUI/src/commonMain/composeResources/values-fa-rIR/strings.xml b/commonsUI/src/commonMain/composeResources/values-fa-rIR/strings.xml index a92f92d82a..df5f3b4363 100644 --- a/commonsUI/src/commonMain/composeResources/values-fa-rIR/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-fa-rIR/strings.xml @@ -10,7 +10,7 @@ <!-- Accessibility --> <!-- Relay history paging (shared feed markers + status card) --> <!-- Notes & Replies --> - <string name="replying_to"> پاسخ به</string> + <string name="replying_to">پاسخ به</string> <!-- Static sites (NIP-5A) & napplets (NIP-5D) feed card --> <string name="napplet_card_permissions">مجوزها:</string> <string name="nsite_root_site">سایت اصلی</string> @@ -226,7 +226,7 @@ <string name="copy_user_id">کپی شناسه کاربر</string> <string name="unblock_user">از بلاک درآوردن کاربر</string> <string name="user_is_blocked_hidden">این کاربر مسدود/بی‌صدا شده است. پست‌های آن‌ها مخفی است.</string> - <string name="npub_hex_username">npub, hex, نام کاربری </string> + <string name="npub_hex_username">npub, hex, نام کاربری</string> <string name="clear">پاک کردن</string> <string name="app_logo">لوگوی اپ</string> <string name="nsec_npub_hex_private_key">nsec / npub / hex کلید خصوصی</string> @@ -248,7 +248,7 @@ <string name="chats_reply_searching_history">در حال جستجوی پیام اصلی…</string> <string name="chats_reply_not_found">این پیام پیدا نشد</string> <string name="chats_reply_searched">همه relay ها جستجو شد · برای مشاهده ضربه بزنید</string> - <string name="error_loading_replies"> :خطا در بارگیری پاسخ ها</string> + <string name="error_loading_replies">:خطا در بارگیری پاسخ ها</string> <string name="try_again">دوباره تلاش کنید</string> <string name="notification_feed_is_empty">هنوز هیچ اعلانی نیست.</string> <string name="open_poll">نظرسنجی باز</string> @@ -274,14 +274,8 @@ <string name="mark_all_known_as_read">همه شناس ها خوانده شده کن</string> <string name="mark_all_new_as_read">همه جدیدها را خوانده شده کن</string> <string name="mark_all_as_read">همه را خوانده شده کن</string> - <string name="account_backup_tips2_md">نکات ایمنی و ذخیره کلید - -حساب کاربری شما با یک کلید خصوصی محافظت می شود. این کلید دنباله ای بلند از کاراکترها است که با **nsec1** آغاز می گردد. هر کس به این کلید خصوصی دسترسی داشته باشد می تواند بجای شما یادداشت پست کرده یا هویت شما را تغییر دهد. -هرگز کلید خصوصی خود را در هیچ وبسایت با نرم افزاری که به آن اطمینان ندارید **وارد نکنید**. -سازندگان امتیست **هرگز** کلیدتان را از شما نمی خواهند. -یک پشتیبان یدکی مخفی از کلید خود به منظور بازیابی **نگه دارید**. ما استفاده از نرم افزار مدیریت پسورد را توصیه می کنیم.</string> - <string name="account_backup_tips3_md">برای ایمنی بیشتر، می توانید کلید خود را با یک رمزعبور رمزنگاری کنید. این نوع کلید با **ncryptsec1** آغاز می شود و نمی تواند بدون رمز عبور شما استفاده شود. -اگر پسوردتان را گم کنید، نخواهید توانست کلیدتان را بازیابی کنید.</string> + <string name="account_backup_tips2_md">نکات ایمنی و ذخیره کلید حساب کاربری شما با یک کلید خصوصی محافظت می شود. این کلید دنباله ای بلند از کاراکترها است که با **nsec1** آغاز می گردد. هر کس به این کلید خصوصی دسترسی داشته باشد می تواند بجای شما یادداشت پست کرده یا هویت شما را تغییر دهد. هرگز کلید خصوصی خود را در هیچ وبسایت با نرم افزاری که به آن اطمینان ندارید **وارد نکنید**. سازندگان امتیست **هرگز** کلیدتان را از شما نمی خواهند. یک پشتیبان یدکی مخفی از کلید خود به منظور بازیابی **نگه دارید**. ما استفاده از نرم افزار مدیریت پسورد را توصیه می کنیم.</string> + <string name="account_backup_tips3_md">برای ایمنی بیشتر، می توانید کلید خود را با یک رمزعبور رمزنگاری کنید. این نوع کلید با **ncryptsec1** آغاز می شود و نمی تواند بدون رمز عبور شما استفاده شود. اگر پسوردتان را گم کنید، نخواهید توانست کلیدتان را بازیابی کنید.</string> <string name="encrypt_and_copy_my_secret_key">رمزنگاری و کپی کلید من</string> <string name="badge_award_image_for">تصویر مدال پاداش برای %1$s</string> <string name="badge_award_image">تصویر جایزه نشان</string> @@ -301,7 +295,7 @@ <string name="quick_action_request_deletion_gallery_title">حذف از گالری</string> <string name="quick_action_request_deletion_gallery_alert_body_v2">این رسانه را از گالری خود حذف کنید.</string> <string name="quick_action_request_deletion_alert_title">درخواست حذف</string> - <string name="quick_action_request_deletion_alert_body">آماتیست درخواست می کند که یادداشت شما از رله هایی که درحال حاضر به آن متصل هستید حذف شود. هیچ تضمینی نیست که یادداشت شما برای همیشه از آن رله ها یا از رله های دیگری که ممکن است در آنها ذخیره شده باشد حذف خواهد شد.. </string> + <string name="quick_action_request_deletion_alert_body">آماتیست درخواست می کند که یادداشت شما از رله هایی که درحال حاضر به آن متصل هستید حذف شود. هیچ تضمینی نیست که یادداشت شما برای همیشه از آن رله ها یا از رله های دیگری که ممکن است در آنها ذخیره شده باشد حذف خواهد شد..</string> <string name="quick_action_block_dialog_btn">بلاک</string> <string name="quick_action_delete_dialog_btn">حذف</string> <string name="quick_action_block">بلاک</string> @@ -970,9 +964,7 @@ <string name="push_server_none_explainer">غیرفعال سازی اعلان ها</string> <string name="push_server_uses_app_explainer">از اپلیکیشن %1$s استفاده می کند</string> <string name="push_server_install_app">تنظیمات اعلان ها</string> - <string name="push_server_install_app_description">برای دریافت اعلان ها، می توانید هر اپلیکیشنی که از [Unified Push](https://unifiedpush.org/) پشتیبانی می کند نصب کنید، مانند[Ntfy](https://ntfy.sh/). - پس از نصب، اپلیکیشنی را که می خواهید استفاده کنید در تنظیمات انتخاب کنید. - </string> + <string name="push_server_install_app_description">برای دریافت اعلان ها، می توانید هر اپلیکیشنی که از [Unified Push](https://unifiedpush.org/) پشتیبانی می کند نصب کنید، مانند[Ntfy](https://ntfy.sh/). پس از نصب، اپلیکیشنی را که می خواهید استفاده کنید در تنظیمات انتخاب کنید.</string> <string name="thread_title">رسته</string> <string name="send_the_seller_a_message">به فروشنده پیغام دهید</string> <string name="hi_there_is_this_still_available">سلام، هنوز موجوده؟</string> @@ -1315,10 +1307,7 @@ <string name="show_nprofile_as_a_qr_code">نمایش nprofile به عنوان کد QR</string> <string name="dm_relays_not_found">تنظیم رله های صندوق دریافت خصوصی</string> <string name="dm_relays_not_found_description">با این تنظیمات همه می توانند بدانند که هنگام ارسال پیام به شما از کدام رله ها استفاده کنند. بدون این تنظیمات ممکن است برخی پیام ها را از دست بدهید.</string> - <string name="dm_relays_not_found_examples2">گزینه های خوب: -- inbox.nostr.wine (پولی) -- auth.nostr1.com (رایگان) -- you.nostr1.com (رله شخصی - پولی)</string> + <string name="dm_relays_not_found_examples2">گزینه های خوب: - inbox.nostr.wine (پولی) - auth.nostr1.com (رایگان) - you.nostr1.com (رله شخصی - پولی)</string> <string name="dm_relays_not_found_editing">از ۱ تا ۳ رله به عنوان رله خصوصی وارد کنید. رله های صندوق دربافت پیام های خصوصی می بایست هر پیامی از هرکس را بپذیرند، ولی تنها به شما اجازه بارگیری پیام ها را می دهند.</string> <string name="dm_relays_not_found_create_now">هم اکنون تنظیم شود</string> <string name="recipient_missing_dm_relays">relay های صندوق ورودی DM یافت نشد. پیام‌ها تحویل داده نمی‌شوند تا زمانی که لیست relay خود را پیکربندی کنند.</string> @@ -1345,7 +1334,7 @@ <string name="zap_the_devs_title">به توسعه دهندگان رپ دهید!</string> <string name="zap_the_devs_description">هدیه شما به ما برای تاثیرگذاری کمک می کند. هر ساتوشی به حساب می آید!</string> <string name="donate_now">اکنون کمک کنید</string> - <string name="brought_to_you_by">تقدیم از طرف: </string> + <string name="brought_to_you_by">تقدیم از طرف:</string> <string name="this_version_brought_to_you_by">این نسخه نرم‌افزار پیشکش از:</string> <string name="version_name">نسخه %1$s</string> <string name="thank_you">متشکریم!</string> @@ -2025,7 +2014,7 @@ <string name="copied_note_id_to_clipboard">(@note1)شناسه یادداشت در کلیپبورد کپی شد</string> <string name="copied_note_text_to_clipboard">یادداشت در کلیپبورد کپی شد</string> <string name="copied_to_clipboard">در کلیپ‌بورد کپی شد</string> - <string name="copied_user_id_to_clipboard">@npubنویسنده در کلیپبورد کپی شد </string> + <string name="copied_user_id_to_clipboard">@npubنویسنده در کلیپبورد کپی شد</string> <string name="copy_my_secret_key">کلید خصوصیم را کپی کن</string> <string name="copy_to_clipboard">کپی به کلیپ‌بورد</string> <string name="could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup">LNUrl از آدرس لایتنینگ "%1$s" بدست نیامد. تنظیمات کاربر را بررسی کنید.</string> @@ -2034,15 +2023,7 @@ <string name="could_not_fetch_invoice_from_details">صورتحساب از %1$s گرفته نشد: %2$s</string> <string name="could_not_open_the_compressed_file">فایل فشرده باز نشد</string> <string name="couldnt_find_nwc_wallets">کیف پول یافت نشد</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst هیچ برنامه کیف پولی که از Nostr Wallet Connect (NWC) پشتیبانی کند پیدا نکرد. -\n\nکارهایی که می‌توانید انجام دهید: -\n\t1. بررسی پشتیبانی NWC: مطمئن شوید که برنامه کیف پول مورد نظر شما با Nostr Wallet Connect سازگار است. -\n\t2. ایجاد اتصال: به برنامه کیف پول خود بروید و یک اتصال NWC جدید ایجاد کنید. -\n\t3. اتصال به Amethyst: کیف پول شما معمولاً سه گزینه ارائه می‌دهد: -\n\t\t3.1 اتصال خودکار: دکمه‌ای برای باز کردن و اتصال خودکار به Amethyst. اگر این گزینه وجود دارد، آن دکمه را بزنید، Amethyst را باز کنید و روی صفحه جدید ذخیره کنید. -\n\t\t3.2 اتصال دستی: یک URI اتصال که می‌توانید کپی کنید و با کلیک روی آیکون paste در این صفحه مستقیماً وارد کنید. -\n\t\t3.3 کد QR: کیف پول یک کد QR ایجاد می‌کند که می‌توانید با دکمه کد QR در این صفحه آن را بخوانید. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst هیچ برنامه کیف پولی که از Nostr Wallet Connect (NWC) پشتیبانی کند پیدا نکرد. \n\nکارهایی که می‌توانید انجام دهید: \n\t1. بررسی پشتیبانی NWC: مطمئن شوید که برنامه کیف پول مورد نظر شما با Nostr Wallet Connect سازگار است. \n\t2. ایجاد اتصال: به برنامه کیف پول خود بروید و یک اتصال NWC جدید ایجاد کنید. \n\t3. اتصال به Amethyst: کیف پول شما معمولاً سه گزینه ارائه می‌دهد: \n\t\t3.1 اتصال خودکار: دکمه‌ای برای باز کردن و اتصال خودکار به Amethyst. اگر این گزینه وجود دارد، آن دکمه را بزنید، Amethyst را باز کنید و روی صفحه جدید ذخیره کنید. \n\t\t3.2 اتصال دستی: یک URI اتصال که می‌توانید کپی کنید و با کلیک روی آیکون paste در این صفحه مستقیماً وارد کنید. \n\t\t3.3 کد QR: کیف پول یک کد QR ایجاد می‌کند که می‌توانید با دکمه کد QR در این صفحه آن را بخوانید.</string> <string name="create">ساختن</string> <string name="d">روز</string> <string name="danger_zone">منطقه خطر</string> @@ -2689,10 +2670,7 @@ <string name="privacy_policy">سیاست حریم خصوصی</string> <string name="private_conversation_notification"><پیام خصوصی رمزگشایی نشد>\\n\nدر یک پیام خصوصی/رمزنگاری شده بین %1$s و %2$s نام شما آمده بود.</string> <string name="private_inbox_section">رله های صندوق دریافت پیام خصوصی</string> - <string name="private_inbox_section_explainer">بین ۱-۳ رله به عنوان رله صندوق ورودی خصوصی وارد کنید. دیگران از این رله ها برای ارسال پیام خصوصی به شما استفاده خواهند کرد. رله های صندوق دریافت پیام خصوصی می بایست هر پیامی از هر کسی را بپذیرند، اما فقط بگذارند شما آن ها را بارگیری کنید. انتخاب های خوب شامل: - - inbox.nostr.wine (پولی) -- auth.nostr1.com (رایگان) -- you.nostr1.com (رله پولی یا شخصی)</string> + <string name="private_inbox_section_explainer">بین ۱-۳ رله به عنوان رله صندوق ورودی خصوصی وارد کنید. دیگران از این رله ها برای ارسال پیام خصوصی به شما استفاده خواهند کرد. رله های صندوق دریافت پیام خصوصی می بایست هر پیامی از هر کسی را بپذیرند، اما فقط بگذارند شما آن ها را بارگیری کنید. انتخاب های خوب شامل: - inbox.nostr.wine (پولی) - auth.nostr1.com (رایگان) - you.nostr1.com (رله پولی یا شخصی)</string> <string name="private_inbox_section_explainer_profile">کاربر DM ها را روی این relay ها دریافت می‌کند</string> <string name="private_message">پیام خصوصی</string> <string name="private_note">خصوصی کردن: یادداشت را به صورت gift-wrap فقط برای کاربران مطلع‌شده ارسال کن</string> @@ -2713,11 +2691,8 @@ <string name="proxy_section">relay های پراکسی</string> <string name="proxy_section_explainer">relay های تجمیع‌کننده که برنامه باید برای دانلود فیدهای شما از آن‌ها استفاده کند، مانند filter.nostr.wine. این جایگزین مدل outbox می‌شود و باعث می‌شود برنامه فقط به relay های موجود در لیست‌هایتان متصل شود.</string> <string name="public_chat">گفتگوی عمومی</string> - <string name="public_chat_explainer">گفتگوهای عمومی در ناستر برای همه نمایان هستند - می توانند در آن شرکت کنند. آنها برای انجمن های باز درباره موضوعات به‌خصوص عالی هستند. - نظارت می تواند توسط حذف پست ها از رله ها انجام گیرد</string> - <string name="public_chat_relays_explainer">برای میزبانی این گروه ۱تا ۳ رله وارد کنید. - کلاینت های ناستر از این تنظیمات استفاده می کنند تا بدانند پیام ها را از کجا بارگیری کنند و به کجا بفرستند.</string> + <string name="public_chat_explainer">گفتگوهای عمومی در ناستر برای همه نمایان هستند می توانند در آن شرکت کنند. آنها برای انجمن های باز درباره موضوعات به‌خصوص عالی هستند. نظارت می تواند توسط حذف پست ها از رله ها انجام گیرد</string> + <string name="public_chat_relays_explainer">برای میزبانی این گروه ۱تا ۳ رله وارد کنید. کلاینت های ناستر از این تنظیمات استفاده می کنند تا بدانند پیام ها را از کجا بارگیری کنند و به کجا بفرستند.</string> <string name="public_chat_relays_title">رله ها</string> <string name="public_chat_title">فراداده گفتگوی عمومی</string> <string name="public_chats">گفتگوهای عمومی</string> @@ -2742,10 +2717,7 @@ <string name="read_only_user">کاربر با امکان خواندن فقط</string> <string name="record_a_message_description">برای ضبط پیام کلیک کنید و نگه دارید</string> <string name="relay_chat">گفتگوی relay</string> - <string name="relay_chat_explainer">گفتگوهای relay گروه‌های چت هستند که توسط relay خانگی خود کنترل می‌شوند. - آن‌ها برای همه در Nostr قابل مشاهده هستند و هر کسی می‌تواند در آن‌ها شرکت کند. - آن‌ها برای جوامع باز حول موضوعات خاص عالی هستند. برخی از این گروه‌ها ناپایدار - هستند و بنابراین پیام‌های چت با گذشت زمان ناپدید می‌شوند</string> + <string name="relay_chat_explainer">گفتگوهای relay گروه‌های چت هستند که توسط relay خانگی خود کنترل می‌شوند. آن‌ها برای همه در Nostr قابل مشاهده هستند و هر کسی می‌تواند در آن‌ها شرکت کند. آن‌ها برای جوامع باز حول موضوعات خاص عالی هستند. برخی از این گروه‌ها ناپایدار هستند و بنابراین پیام‌های چت با گذشت زمان ناپدید می‌شوند</string> <string name="relay_chat_title">گفتگوهای relay</string> <string name="relay_management_tab_events">رویدادها</string> <string name="relay_management_tab_ips">آدرس‌های IP</string> @@ -2903,7 +2875,7 @@ <string name="tor_use_new_relays_explainer">تور را برای رله های ارسال/دریافت الزامی کن</string> <string name="tor_use_nip05_verification">تایید آدرس ناستر</string> <string name="tor_use_nip05_verification_explainer">تور را برای تایید آدرس های NIP-05 الزامی کن</string> - <string name="tor_use_onion_address">رله ها / آدرس های Onion </string> + <string name="tor_use_onion_address">رله ها / آدرس های Onion</string> <string name="tor_use_onion_address_explainer">از تور برای هر آدرس اونیون استفاده کن</string> <string name="tor_use_profile_pictures">تصاویر نمایه</string> <string name="tor_use_profile_pictures_explainer">تور را برای بارگیری تصاویر نمایه اجباری کن</string> diff --git a/commonsUI/src/commonMain/composeResources/values-fa/strings.xml b/commonsUI/src/commonMain/composeResources/values-fa/strings.xml index a20d36b070..cf8347e236 100644 --- a/commonsUI/src/commonMain/composeResources/values-fa/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-fa/strings.xml @@ -88,7 +88,7 @@ <string name="quote">نقل قول</string> <string name="fork">فورک</string> <string name="new_amount_in_sats">مبلغ جدید به ساتوشی</string> - <string name="replying_to"> پاسخ به</string> + <string name="replying_to">پاسخ به</string> <string name="and"> و </string> <string name="following">دنبال شونده</string> <string name="followers">دنبال کننده</string> @@ -211,7 +211,7 @@ <string name="copy_user_id">کپی شناسه کاربر</string> <string name="unblock_user">از بلاک درآوردن کاربر</string> <string name="user_is_blocked_hidden">این کاربر مسدود/بی‌صدا شده است. پست‌های آن‌ها مخفی است.</string> - <string name="npub_hex_username">npub, hex, نام کاربری </string> + <string name="npub_hex_username">npub, hex, نام کاربری</string> <string name="clear">پاک کردن</string> <string name="app_logo">لوگوی اپ</string> <string name="nsec_npub_hex_private_key">nsec / npub / hex کلید خصوصی</string> @@ -233,7 +233,7 @@ <string name="chats_reply_searching_history">در حال جستجوی پیام اصلی…</string> <string name="chats_reply_not_found">این پیام پیدا نشد</string> <string name="chats_reply_searched">همه relay ها جستجو شد · برای مشاهده ضربه بزنید</string> - <string name="error_loading_replies"> :خطا در بارگیری پاسخ ها</string> + <string name="error_loading_replies">:خطا در بارگیری پاسخ ها</string> <string name="try_again">دوباره تلاش کنید</string> <string name="notification_feed_is_empty">هنوز هیچ اعلانی نیست.</string> <string name="open_poll">نظرسنجی باز</string> @@ -259,14 +259,8 @@ <string name="mark_all_known_as_read">همه شناس ها خوانده شده کن</string> <string name="mark_all_new_as_read">همه جدیدها را خوانده شده کن</string> <string name="mark_all_as_read">همه را خوانده شده کن</string> - <string name="account_backup_tips2_md">نکات ایمنی و ذخیره کلید - -حساب کاربری شما با یک کلید خصوصی محافظت می شود. این کلید دنباله ای بلند از کاراکترها است که با **nsec1** آغاز می گردد. هر کس به این کلید خصوصی دسترسی داشته باشد می تواند بجای شما یادداشت پست کرده یا هویت شما را تغییر دهد. -هرگز کلید خصوصی خود را در هیچ وبسایت با نرم افزاری که به آن اطمینان ندارید **وارد نکنید**. -سازندگان امتیست **هرگز** کلیدتان را از شما نمی خواهند. -یک پشتیبان یدکی مخفی از کلید خود به منظور بازیابی **نگه دارید**. ما استفاده از نرم افزار مدیریت پسورد را توصیه می کنیم.</string> - <string name="account_backup_tips3_md">برای ایمنی بیشتر، می توانید کلید خود را با یک رمزعبور رمزنگاری کنید. این نوع کلید با **ncryptsec1** آغاز می شود و نمی تواند بدون رمز عبور شما استفاده شود. -اگر پسوردتان را گم کنید، نخواهید توانست کلیدتان را بازیابی کنید.</string> + <string name="account_backup_tips2_md">نکات ایمنی و ذخیره کلید حساب کاربری شما با یک کلید خصوصی محافظت می شود. این کلید دنباله ای بلند از کاراکترها است که با **nsec1** آغاز می گردد. هر کس به این کلید خصوصی دسترسی داشته باشد می تواند بجای شما یادداشت پست کرده یا هویت شما را تغییر دهد. هرگز کلید خصوصی خود را در هیچ وبسایت با نرم افزاری که به آن اطمینان ندارید **وارد نکنید**. سازندگان امتیست **هرگز** کلیدتان را از شما نمی خواهند. یک پشتیبان یدکی مخفی از کلید خود به منظور بازیابی **نگه دارید**. ما استفاده از نرم افزار مدیریت پسورد را توصیه می کنیم.</string> + <string name="account_backup_tips3_md">برای ایمنی بیشتر، می توانید کلید خود را با یک رمزعبور رمزنگاری کنید. این نوع کلید با **ncryptsec1** آغاز می شود و نمی تواند بدون رمز عبور شما استفاده شود. اگر پسوردتان را گم کنید، نخواهید توانست کلیدتان را بازیابی کنید.</string> <string name="encrypt_and_copy_my_secret_key">رمزنگاری و کپی کلید من</string> <string name="badge_award_image_for">تصویر مدال پاداش برای %1$s</string> <string name="badge_award_image">تصویر جایزه نشان</string> @@ -286,7 +280,7 @@ <string name="quick_action_request_deletion_gallery_title">حذف از گالری</string> <string name="quick_action_request_deletion_gallery_alert_body_v2">این رسانه را از گالری خود حذف کنید.</string> <string name="quick_action_request_deletion_alert_title">درخواست حذف</string> - <string name="quick_action_request_deletion_alert_body">آماتیست درخواست می کند که یادداشت شما از رله هایی که درحال حاضر به آن متصل هستید حذف شود. هیچ تضمینی نیست که یادداشت شما برای همیشه از آن رله ها یا از رله های دیگری که ممکن است در آنها ذخیره شده باشد حذف خواهد شد.. </string> + <string name="quick_action_request_deletion_alert_body">آماتیست درخواست می کند که یادداشت شما از رله هایی که درحال حاضر به آن متصل هستید حذف شود. هیچ تضمینی نیست که یادداشت شما برای همیشه از آن رله ها یا از رله های دیگری که ممکن است در آنها ذخیره شده باشد حذف خواهد شد..</string> <string name="quick_action_block_dialog_btn">بلاک</string> <string name="quick_action_delete_dialog_btn">حذف</string> <string name="quick_action_block">بلاک</string> @@ -959,9 +953,7 @@ <string name="push_server_none_explainer">غیرفعال سازی اعلان ها</string> <string name="push_server_uses_app_explainer">از اپلیکیشن %1$s استفاده می کند</string> <string name="push_server_install_app">تنظیمات اعلان ها</string> - <string name="push_server_install_app_description">برای دریافت اعلان ها، می توانید هر اپلیکیشنی که از [Unified Push](https://unifiedpush.org/) پشتیبانی می کند نصب کنید، مانند[Ntfy](https://ntfy.sh/). - پس از نصب، اپلیکیشنی را که می خواهید استفاده کنید در تنظیمات انتخاب کنید. - </string> + <string name="push_server_install_app_description">برای دریافت اعلان ها، می توانید هر اپلیکیشنی که از [Unified Push](https://unifiedpush.org/) پشتیبانی می کند نصب کنید، مانند[Ntfy](https://ntfy.sh/). پس از نصب، اپلیکیشنی را که می خواهید استفاده کنید در تنظیمات انتخاب کنید.</string> <string name="thread_title">رسته</string> <string name="send_the_seller_a_message">به فروشنده پیغام دهید</string> <string name="hi_there_is_this_still_available">سلام، هنوز موجوده؟</string> @@ -1313,10 +1305,7 @@ <string name="show_nprofile_as_a_qr_code">نمایش nprofile به عنوان کد QR</string> <string name="dm_relays_not_found">تنظیم رله های صندوق دریافت خصوصی</string> <string name="dm_relays_not_found_description">با این تنظیمات همه می توانند بدانند که هنگام ارسال پیام به شما از کدام رله ها استفاده کنند. بدون این تنظیمات ممکن است برخی پیام ها را از دست بدهید.</string> - <string name="dm_relays_not_found_examples2">گزینه های خوب: -- inbox.nostr.wine (پولی) -- auth.nostr1.com (رایگان) -- you.nostr1.com (رله شخصی - پولی)</string> + <string name="dm_relays_not_found_examples2">گزینه های خوب: - inbox.nostr.wine (پولی) - auth.nostr1.com (رایگان) - you.nostr1.com (رله شخصی - پولی)</string> <string name="dm_relays_not_found_editing">از ۱ تا ۳ رله به عنوان رله خصوصی وارد کنید. رله های صندوق دربافت پیام های خصوصی می بایست هر پیامی از هرکس را بپذیرند، ولی تنها به شما اجازه بارگیری پیام ها را می دهند.</string> <string name="dm_relays_not_found_create_now">هم اکنون تنظیم شود</string> <string name="recipient_missing_dm_relays">relay های صندوق ورودی DM یافت نشد. پیام‌ها تحویل داده نمی‌شوند تا زمانی که لیست relay خود را پیکربندی کنند.</string> @@ -1343,7 +1332,7 @@ <string name="zap_the_devs_title">به توسعه دهندگان رپ دهید!</string> <string name="zap_the_devs_description">هدیه شما به ما برای تاثیرگذاری کمک می کند. هر ساتوشی به حساب می آید!</string> <string name="donate_now">اکنون کمک کنید</string> - <string name="brought_to_you_by">تقدیم از طرف: </string> + <string name="brought_to_you_by">تقدیم از طرف:</string> <string name="this_version_brought_to_you_by">این نسخه نرم‌افزار پیشکش از:</string> <string name="version_name">نسخه %1$s</string> <string name="thank_you">متشکریم!</string> @@ -2048,7 +2037,7 @@ <string name="copied_note_id_to_clipboard">(@note1)شناسه یادداشت در کلیپبورد کپی شد</string> <string name="copied_note_text_to_clipboard">یادداشت در کلیپبورد کپی شد</string> <string name="copied_to_clipboard">در کلیپ‌بورد کپی شد</string> - <string name="copied_user_id_to_clipboard">@npubنویسنده در کلیپبورد کپی شد </string> + <string name="copied_user_id_to_clipboard">@npubنویسنده در کلیپبورد کپی شد</string> <string name="copy_my_secret_key">کلید خصوصیم را کپی کن</string> <string name="copy_to_clipboard">کپی به کلیپ‌بورد</string> <string name="could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup">LNUrl از آدرس لایتنینگ "%1$s" بدست نیامد. تنظیمات کاربر را بررسی کنید.</string> @@ -2057,15 +2046,7 @@ <string name="could_not_fetch_invoice_from_details">صورتحساب از %1$s گرفته نشد: %2$s</string> <string name="could_not_open_the_compressed_file">فایل فشرده باز نشد</string> <string name="couldnt_find_nwc_wallets">کیف پول یافت نشد</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst هیچ برنامه کیف پولی که از Nostr Wallet Connect (NWC) پشتیبانی کند پیدا نکرد. -\n\nکارهایی که می‌توانید انجام دهید: -\n\t1. بررسی پشتیبانی NWC: مطمئن شوید که برنامه کیف پول مورد نظر شما با Nostr Wallet Connect سازگار است. -\n\t2. ایجاد اتصال: به برنامه کیف پول خود بروید و یک اتصال NWC جدید ایجاد کنید. -\n\t3. اتصال به Amethyst: کیف پول شما معمولاً سه گزینه ارائه می‌دهد: -\n\t\t3.1 اتصال خودکار: دکمه‌ای برای باز کردن و اتصال خودکار به Amethyst. اگر این گزینه وجود دارد، آن دکمه را بزنید، Amethyst را باز کنید و روی صفحه جدید ذخیره کنید. -\n\t\t3.2 اتصال دستی: یک URI اتصال که می‌توانید کپی کنید و با کلیک روی آیکون paste در این صفحه مستقیماً وارد کنید. -\n\t\t3.3 کد QR: کیف پول یک کد QR ایجاد می‌کند که می‌توانید با دکمه کد QR در این صفحه آن را بخوانید. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst هیچ برنامه کیف پولی که از Nostr Wallet Connect (NWC) پشتیبانی کند پیدا نکرد. \n\nکارهایی که می‌توانید انجام دهید: \n\t1. بررسی پشتیبانی NWC: مطمئن شوید که برنامه کیف پول مورد نظر شما با Nostr Wallet Connect سازگار است. \n\t2. ایجاد اتصال: به برنامه کیف پول خود بروید و یک اتصال NWC جدید ایجاد کنید. \n\t3. اتصال به Amethyst: کیف پول شما معمولاً سه گزینه ارائه می‌دهد: \n\t\t3.1 اتصال خودکار: دکمه‌ای برای باز کردن و اتصال خودکار به Amethyst. اگر این گزینه وجود دارد، آن دکمه را بزنید، Amethyst را باز کنید و روی صفحه جدید ذخیره کنید. \n\t\t3.2 اتصال دستی: یک URI اتصال که می‌توانید کپی کنید و با کلیک روی آیکون paste در این صفحه مستقیماً وارد کنید. \n\t\t3.3 کد QR: کیف پول یک کد QR ایجاد می‌کند که می‌توانید با دکمه کد QR در این صفحه آن را بخوانید.</string> <string name="create">ساختن</string> <string name="d">روز</string> <string name="danger_zone">منطقه خطر</string> @@ -2732,10 +2713,7 @@ <string name="privacy_policy">سیاست حریم خصوصی</string> <string name="private_conversation_notification"><پیام خصوصی رمزگشایی نشد>\\n\nدر یک پیام خصوصی/رمزنگاری شده بین %1$s و %2$s نام شما آمده بود.</string> <string name="private_inbox_section">رله های صندوق دریافت پیام خصوصی</string> - <string name="private_inbox_section_explainer">بین ۱-۳ رله به عنوان رله صندوق ورودی خصوصی وارد کنید. دیگران از این رله ها برای ارسال پیام خصوصی به شما استفاده خواهند کرد. رله های صندوق دریافت پیام خصوصی می بایست هر پیامی از هر کسی را بپذیرند، اما فقط بگذارند شما آن ها را بارگیری کنید. انتخاب های خوب شامل: - - inbox.nostr.wine (پولی) -- auth.nostr1.com (رایگان) -- you.nostr1.com (رله پولی یا شخصی)</string> + <string name="private_inbox_section_explainer">بین ۱-۳ رله به عنوان رله صندوق ورودی خصوصی وارد کنید. دیگران از این رله ها برای ارسال پیام خصوصی به شما استفاده خواهند کرد. رله های صندوق دریافت پیام خصوصی می بایست هر پیامی از هر کسی را بپذیرند، اما فقط بگذارند شما آن ها را بارگیری کنید. انتخاب های خوب شامل: - inbox.nostr.wine (پولی) - auth.nostr1.com (رایگان) - you.nostr1.com (رله پولی یا شخصی)</string> <string name="private_inbox_section_explainer_profile">کاربر DM ها را روی این relay ها دریافت می‌کند</string> <string name="private_message">پیام خصوصی</string> <string name="private_note">خصوصی کردن: یادداشت را به صورت gift-wrap فقط برای کاربران مطلع‌شده ارسال کن</string> @@ -2782,10 +2760,7 @@ <string name="read_only_user">کاربر با امکان خواندن فقط</string> <string name="record_a_message_description">برای ضبط پیام کلیک کنید و نگه دارید</string> <string name="relay_chat">گفتگوی relay</string> - <string name="relay_chat_explainer">گفتگوهای relay گروه‌های چت هستند که توسط relay خانگی خود کنترل می‌شوند. - آن‌ها برای همه در Nostr قابل مشاهده هستند و هر کسی می‌تواند در آن‌ها شرکت کند. - آن‌ها برای جوامع باز حول موضوعات خاص عالی هستند. برخی از این گروه‌ها ناپایدار - هستند و بنابراین پیام‌های چت با گذشت زمان ناپدید می‌شوند</string> + <string name="relay_chat_explainer">گفتگوهای relay گروه‌های چت هستند که توسط relay خانگی خود کنترل می‌شوند. آن‌ها برای همه در Nostr قابل مشاهده هستند و هر کسی می‌تواند در آن‌ها شرکت کند. آن‌ها برای جوامع باز حول موضوعات خاص عالی هستند. برخی از این گروه‌ها ناپایدار هستند و بنابراین پیام‌های چت با گذشت زمان ناپدید می‌شوند</string> <string name="relay_chat_title">گفتگوهای relay</string> <string name="relay_management_tab_events">رویدادها</string> <string name="relay_management_tab_ips">آدرس‌های IP</string> @@ -2943,7 +2918,7 @@ <string name="tor_use_new_relays_explainer">تور را برای رله های ارسال/دریافت الزامی کن</string> <string name="tor_use_nip05_verification">تایید آدرس ناستر</string> <string name="tor_use_nip05_verification_explainer">تور را برای تایید آدرس های NIP-05 الزامی کن</string> - <string name="tor_use_onion_address">رله ها / آدرس های Onion </string> + <string name="tor_use_onion_address">رله ها / آدرس های Onion</string> <string name="tor_use_onion_address_explainer">از تور برای هر آدرس اونیون استفاده کن</string> <string name="tor_use_profile_pictures">تصاویر نمایه</string> <string name="tor_use_profile_pictures_explainer">تور را برای بارگیری تصاویر نمایه اجباری کن</string> diff --git a/commonsUI/src/commonMain/composeResources/values-fi-rFI/strings.xml b/commonsUI/src/commonMain/composeResources/values-fi-rFI/strings.xml index ede84f29eb..f2784cced4 100644 --- a/commonsUI/src/commonMain/composeResources/values-fi-rFI/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-fi-rFI/strings.xml @@ -268,15 +268,8 @@ <string name="mark_all_known_as_read">Merkitse kaikki Tunnetut luetuiksi</string> <string name="mark_all_new_as_read">Merkitse kaikki Uudet luetuiksi</string> <string name="mark_all_as_read">Merkitse kaikki luetuiksi</string> - <string name="account_backup_tips2_md"> ## Avainten varmuuskopiointi ja turvallisuusvinkit - \n\nTilisi on suojattu salaisella avaimella. Avain on pitkä merkkijono, joka alkaa **nsec1**:llä. Kuka tahansa, jolla on pääsy tähän salaiseen avaimeen, voi julkaista ja muuttaa henkilöllisyyttäsi. - \n\n- **Älä** anna salaista avaintasi mihinkään verkkosivustoon tai ohjelmistoon, johon et luota. - \n- Amethystin kehittäjät eivät **koskaan** pyydä salaista avaintasi. - \n- **Tee** turvallinen varmuuskopio salaisesta avaimestasi tilin palauttamista varten. Suosittelemme salasananhallintaohjelman käyttöä. - </string> - <string name="account_backup_tips3_md"> Lisäturvallisuuden vuoksi voit salata avaimesi salasanalla. Tämä avain alkaa **ncryptsec1**:llä eikä sitä voi käyttää ilman salasanaasi. - \n\nJos kadotat salasanasi, et pysty palauttamaan avaintasi. - </string> + <string name="account_backup_tips2_md">## Avainten varmuuskopiointi ja turvallisuusvinkit \n\nTilisi on suojattu salaisella avaimella. Avain on pitkä merkkijono, joka alkaa **nsec1**:llä. Kuka tahansa, jolla on pääsy tähän salaiseen avaimeen, voi julkaista ja muuttaa henkilöllisyyttäsi. \n\n- **Älä** anna salaista avaintasi mihinkään verkkosivustoon tai ohjelmistoon, johon et luota. \n- Amethystin kehittäjät eivät **koskaan** pyydä salaista avaintasi. \n- **Tee** turvallinen varmuuskopio salaisesta avaimestasi tilin palauttamista varten. Suosittelemme salasananhallintaohjelman käyttöä.</string> + <string name="account_backup_tips3_md">Lisäturvallisuuden vuoksi voit salata avaimesi salasanalla. Tämä avain alkaa **ncryptsec1**:llä eikä sitä voi käyttää ilman salasanaasi. \n\nJos kadotat salasanasi, et pysty palauttamaan avaintasi.</string> <string name="encrypt_and_copy_my_secret_key">Salaa ja kopioi salainen avaimeni</string> <string name="badge_award_image_for">Merkki kohteelle %1$s</string> <string name="badge_award_image">"Merkkipalkinnon kuva</string> @@ -959,8 +952,7 @@ <string name="push_server_none_explainer">Poistaa Push-ilmoitukset käytöstä</string> <string name="push_server_uses_app_explainer">Käyttää sovellusta %1$s</string> <string name="push_server_install_app">Push-ilmoituksen asennus</string> - <string name="push_server_install_app_description"> Saadaksesi push-ilmoituksia, asenna mikä tahansa sovellus, joka tukee Unified Push:ia, kuten Ntfy. - Asennuksen jälkeen valitse Asetuksista sovellus, jota haluat käyttää.</string> + <string name="push_server_install_app_description">Saadaksesi push-ilmoituksia, asenna mikä tahansa sovellus, joka tukee Unified Push:ia, kuten Ntfy. Asennuksen jälkeen valitse Asetuksista sovellus, jota haluat käyttää.</string> <string name="thread_title">Keskusteluketju</string> <string name="send_the_seller_a_message">Lähetä myyjälle viesti</string> <string name="hi_there_is_this_still_available">Hei, onko tämä vielä saatavilla?</string> @@ -2011,15 +2003,7 @@ <string name="could_not_fetch_invoice_from_details">Laskun hakeminen epäonnistui kohteesta %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Pakattua tiedostoa ei voitu avata</string> <string name="couldnt_find_nwc_wallets">Lompakkoa ei löydy</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst ei löytänyt yhtään lompakkosovellusta, joka tukee Nostr Wallet Connect (NWC) -toimintoa. -\n\nMitä voit tehdä: -\n\t1. Tarkista NWC-tuki: Varmista, että haluamasi lompakkosovellus on yhteensopiva Nostr Wallet Connectin kanssa. -\n\t2. Luo yhteys: Siirry lompakkosovellukseesi ja luo uusi NWC-yhteys. -\n\t3. Linkitä Amethystiin: Lompakkosi tarjoaa yleensä kolme vaihtoehtoa: -\n\t\t3.1 Automaattinen linkitys: Painike, joka avaa Amethystin ja yhdistää automaattisesti. Jos näet sen, paina painiketta, avaa Amethyst ja tallenna uudella näytöllä. -\n\t\t3.2 Manuaalinen linkitys: YhteysURI, jonka voit kopioida ja liittää tämän näytön liitä-kuvakkeella. -\n\t\t3.3 QR-koodi: Lompakko luo QR-koodin, jonka voit lukea tämän näytön QR-koodipainikkeella. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst ei löytänyt yhtään lompakkosovellusta, joka tukee Nostr Wallet Connect (NWC) -toimintoa. \n\nMitä voit tehdä: \n\t1. Tarkista NWC-tuki: Varmista, että haluamasi lompakkosovellus on yhteensopiva Nostr Wallet Connectin kanssa. \n\t2. Luo yhteys: Siirry lompakkosovellukseesi ja luo uusi NWC-yhteys. \n\t3. Linkitä Amethystiin: Lompakkosi tarjoaa yleensä kolme vaihtoehtoa: \n\t\t3.1 Automaattinen linkitys: Painike, joka avaa Amethystin ja yhdistää automaattisesti. Jos näet sen, paina painiketta, avaa Amethyst ja tallenna uudella näytöllä. \n\t\t3.2 Manuaalinen linkitys: YhteysURI, jonka voit kopioida ja liittää tämän näytön liitä-kuvakkeella. \n\t\t3.3 QR-koodi: Lompakko luo QR-koodin, jonka voit lukea tämän näytön QR-koodipainikkeella.</string> <string name="create">Luo</string> <string name="d">pv</string> <string name="danger_zone">Vaara-alue</string> @@ -2677,11 +2661,8 @@ <string name="proxy_section">Proxy-relait</string> <string name="proxy_section_explainer">Aggregaattorirelait, joita sovelluksen täytyy käyttää feedien lataamiseen, kuten filter.nostr.wine. Tämä korvaa outbox-mallin ja saa sovelluksen yhdistämään vain listoissasi oleviin relaihin.</string> <string name="public_chat">Julkinen keskustelu</string> - <string name="public_chat_explainer">Julkiset chatit ovat näkyvissä kaikille Nostrissa ja kuka tahansa - voi osallistua niihin. Ne sopivat hyvin avoimiin yhteisöihin tiettyjen aiheiden ympärille. - Moderointia voidaan hallita poistamalla julkaisuja sen relay-palvelimilta</string> - <string name="public_chat_relays_explainer">Syötä 1–3 relay-palvelinta, jotka isännöivät tätä ryhmää. - Nostr-asiakkaat käyttävät tätä asetusta tietääkseen, mistä ladata viestejä ja mihin lähettää viestisi.</string> + <string name="public_chat_explainer">Julkiset chatit ovat näkyvissä kaikille Nostrissa ja kuka tahansa voi osallistua niihin. Ne sopivat hyvin avoimiin yhteisöihin tiettyjen aiheiden ympärille. Moderointia voidaan hallita poistamalla julkaisuja sen relay-palvelimilta</string> + <string name="public_chat_relays_explainer">Syötä 1–3 relay-palvelinta, jotka isännöivät tätä ryhmää. Nostr-asiakkaat käyttävät tätä asetusta tietääkseen, mistä ladata viestejä ja mihin lähettää viestisi.</string> <string name="public_chat_relays_title">relay-palvelimet</string> <string name="public_chat_title">Julkisen chatin metatiedot</string> <string name="public_chats">Julkiset keskustelut</string> @@ -2706,10 +2687,7 @@ <string name="read_only_user">Vain luku -käyttäjä</string> <string name="record_a_message_description">Paina ja pidä pohjassa tallentaaksesi ääniviesti</string> <string name="relay_chat">relay-chat</string> - <string name="relay_chat_explainer">relay-chatit ovat chat-ryhmiä, joita hallinnoi niiden kotipalvelin. - Ne ovat näkyvissä kaikille Nostrissa ja kuka tahansa voi osallistua niihin. - Ne sopivat hyvin avoimiin yhteisöihin tiettyjen aiheiden ympärille. Osa näistä ryhmistä on katoavia, - joten chat-viestit häviävät ajan myötä</string> + <string name="relay_chat_explainer">relay-chatit ovat chat-ryhmiä, joita hallinnoi niiden kotipalvelin. Ne ovat näkyvissä kaikille Nostrissa ja kuka tahansa voi osallistua niihin. Ne sopivat hyvin avoimiin yhteisöihin tiettyjen aiheiden ympärille. Osa näistä ryhmistä on katoavia, joten chat-viestit häviävät ajan myötä</string> <string name="relay_chat_title">relay-chatit</string> <string name="relay_management_tab_events">Tapahtumat</string> <string name="relay_management_tab_ips">IP-osoitteet</string> diff --git a/commonsUI/src/commonMain/composeResources/values-fr-rCA/strings.xml b/commonsUI/src/commonMain/composeResources/values-fr-rCA/strings.xml index 6dbdac7e80..677d4afb32 100644 --- a/commonsUI/src/commonMain/composeResources/values-fr-rCA/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-fr-rCA/strings.xml @@ -218,7 +218,7 @@ <string name="copy_user_id">Copier l'ID utilisateur</string> <string name="unblock_user">Débloquer l'utilisateur</string> <string name="user_is_blocked_hidden">Cet utilisateur est bloqué/muet. Ses publications sont masquées.</string> - <string name="npub_hex_username">npub, hex, nom d'utilisateur </string> + <string name="npub_hex_username">npub, hex, nom d'utilisateur</string> <string name="clear">Nettoyer</string> <string name="app_logo">Logo de l'App</string> <string name="nsec_npub_hex_private_key">nsec / npub / hex private key</string> @@ -266,15 +266,8 @@ <string name="mark_all_known_as_read">Marquer tous les Connus comme lu</string> <string name="mark_all_new_as_read">Marquer tous les Nouveaux comme lu</string> <string name="mark_all_as_read">Tout marquer comme lu</string> - <string name="account_backup_tips2_md"> ## Sauvegarde des Clés et Conseils de Sécurité - \n\nVotre compte est sécurisé par une clé secrète. La clé est une longue séquence de caractères commençant par **nsec1**. Toute personne ayant accès à cette clé secrète peut publier et modifier votre identité. - \n\n- Ne mettez **pas** votre clé secrète sur un site web ou un logiciel auquel vous ne faites pas confiance. - \n- Les développeurs d'Amethyst ne vous demanderont **jamais** votre clé secrète. - \n- **Faites** une sauvegarde sécurisée de votre clé secrète pour la récupération de votre compte. Nous vous recommandons d'utiliser un gestionnaire de mots de passe. - </string> - <string name="account_backup_tips3_md"> Pour plus de sécurité, vous pouvez chiffrer votre clé avec un mot de passe. Cette clé commence par **ncryptsec1** et ne peut être utilisée sans votre mot de passe. - \n\nSi vous perdez votre mot de passe, vous ne pourrez pas récupérer votre clé. - </string> + <string name="account_backup_tips2_md">## Sauvegarde des Clés et Conseils de Sécurité \n\nVotre compte est sécurisé par une clé secrète. La clé est une longue séquence de caractères commençant par **nsec1**. Toute personne ayant accès à cette clé secrète peut publier et modifier votre identité. \n\n- Ne mettez **pas** votre clé secrète sur un site web ou un logiciel auquel vous ne faites pas confiance. \n- Les développeurs d'Amethyst ne vous demanderont **jamais** votre clé secrète. \n- **Faites** une sauvegarde sécurisée de votre clé secrète pour la récupération de votre compte. Nous vous recommandons d'utiliser un gestionnaire de mots de passe.</string> + <string name="account_backup_tips3_md">Pour plus de sécurité, vous pouvez chiffrer votre clé avec un mot de passe. Cette clé commence par **ncryptsec1** et ne peut être utilisée sans votre mot de passe. \n\nSi vous perdez votre mot de passe, vous ne pourrez pas récupérer votre clé.</string> <string name="encrypt_and_copy_my_secret_key">Chiffrer et copier ma clé secrète</string> <string name="badge_award_image_for">Image de badge pour %1$s</string> <string name="badge_award_image">Image du badge de récompense</string> @@ -823,7 +816,7 @@ <string name="max_subs">Souscription maximale</string> <string name="max_filters_per_sub">Nombre maximum de filtres par sub</string> <string name="max_limit_events_returning">Limite Max (Renvoi des Évènements)</string> - <string name="max_subid_length">Longueur maximale du SubID </string> + <string name="max_subid_length">Longueur maximale du SubID</string> <string name="cashu_mint_label">Mint : %1$s</string> <string name="cashu_redeem">Échanger</string> <string name="cashu_copy_token">Copier le Jeton</string> @@ -938,9 +931,7 @@ <string name="push_server_none_explainer">Désactive les Notifications Push</string> <string name="push_server_uses_app_explainer">Utilise l'application %1$s</string> <string name="push_server_install_app">Configuration des Notifications Push</string> - <string name="push_server_install_app_description"> Pour recevoir des notifications push, installez n'importe quelle application qui supporte les [Unified Push](https://unifiedpush.org/), tel que [Ntfy](https://ntfy.sh/). - Après installation, sélectionnez l'application que vous voulez utiliser dans les Paramètres. - </string> + <string name="push_server_install_app_description">Pour recevoir des notifications push, installez n'importe quelle application qui supporte les [Unified Push](https://unifiedpush.org/), tel que [Ntfy](https://ntfy.sh/). Après installation, sélectionnez l'application que vous voulez utiliser dans les Paramètres.</string> <string name="thread_title">Fil de discussion</string> <string name="send_the_seller_a_message">Envoyer un message au vendeur</string> <string name="hi_there_is_this_still_available">Bonjour, est-ce toujours disponible ?</string> @@ -2603,11 +2594,8 @@ <string name="proxy_section">Relais Proxy</string> <string name="proxy_section_explainer">Relais agrégateurs que l'application doit utiliser pour télécharger vos flux, tels que filter.nostr.wine. Cela remplace le modèle de boîte d'envoi et permettra à l'application de se connecter uniquement aux relais de vos listes.</string> <string name="public_chat">Chat public</string> - <string name="public_chat_explainer">Les conversations publiques sont visibles pour tout le monde sur Nostr et n'importe qui - peut y participer. Elles sont idéales pour les communautés ouvertes autour de sujets spécifiques. - La modération peut être contrôlée en supprimant les messages sur ses relais</string> - <string name="public_chat_relays_explainer">Insérez entre 1 et 3 relais qui hébergent ce groupe. - Les clients Nostr utilisent ce paramètre pour savoir où télécharger les messages et où les envoyer.</string> + <string name="public_chat_explainer">Les conversations publiques sont visibles pour tout le monde sur Nostr et n'importe qui peut y participer. Elles sont idéales pour les communautés ouvertes autour de sujets spécifiques. La modération peut être contrôlée en supprimant les messages sur ses relais</string> + <string name="public_chat_relays_explainer">Insérez entre 1 et 3 relais qui hébergent ce groupe. Les clients Nostr utilisent ce paramètre pour savoir où télécharger les messages et où les envoyer.</string> <string name="public_chat_relays_title">Relais</string> <string name="public_chat_title">Métadonnées de tchat public</string> <string name="public_chats">Chats publics</string> @@ -2632,10 +2620,7 @@ <string name="read_only_user">Utilisateur en lecture seule</string> <string name="record_a_message_description">Cliquez et maintenez pour enregistrer un message</string> <string name="relay_chat">Relais de discussion</string> - <string name="relay_chat_explainer">Les relais de discussions sont des groupes de discussion contrôlés par leur relais d'origine. - Ils sont visibles par tous les utilisateurs de Nostr et tout le monde peut y participer. - Ils sont idéaux pour les communautés ouvertes autour de sujets spécifiques. Certains de ces groupes sont éphémères - et, par conséquent, les messages de discussion disparaissent avec le temps</string> + <string name="relay_chat_explainer">Les relais de discussions sont des groupes de discussion contrôlés par leur relais d'origine. Ils sont visibles par tous les utilisateurs de Nostr et tout le monde peut y participer. Ils sont idéaux pour les communautés ouvertes autour de sujets spécifiques. Certains de ces groupes sont éphémères et, par conséquent, les messages de discussion disparaissent avec le temps</string> <string name="relay_chat_title">Relais de discussions</string> <string name="relay_management_tab_events">Événements</string> <string name="relay_management_tab_pubkeys">Utilisateurs</string> diff --git a/commonsUI/src/commonMain/composeResources/values-fr-rFR/strings.xml b/commonsUI/src/commonMain/composeResources/values-fr-rFR/strings.xml index 41d6f05237..76a4f93daf 100644 --- a/commonsUI/src/commonMain/composeResources/values-fr-rFR/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-fr-rFR/strings.xml @@ -220,7 +220,7 @@ <string name="copy_user_id">Copier l'ID utilisateur</string> <string name="unblock_user">Débloquer l'utilisateur</string> <string name="user_is_blocked_hidden">Cet utilisateur est bloqué/muet. Ses publications sont masquées.</string> - <string name="npub_hex_username">npub, hex, nom d'utilisateur </string> + <string name="npub_hex_username">npub, hex, nom d'utilisateur</string> <string name="clear">Nettoyer</string> <string name="app_logo">Logo de l'App</string> <string name="nsec_npub_hex_private_key">nsec / npub / hex private key</string> @@ -268,15 +268,8 @@ <string name="mark_all_known_as_read">Marquer tous les Connus comme lu</string> <string name="mark_all_new_as_read">Marquer tous les Nouveaux comme lu</string> <string name="mark_all_as_read">Tout marquer comme lu</string> - <string name="account_backup_tips2_md"> ## Sauvegarde des Clés et Conseils de Sécurité - \n\nVotre compte est sécurisé par une clé secrète. La clé est une longue séquence de caractères commençant par **nsec1**. Toute personne ayant accès à cette clé secrète peut publier et modifier votre identité. - \n\n- Ne mettez **pas** votre clé secrète sur un site web ou un logiciel auquel vous ne faites pas confiance. - \n- Les développeurs d'Amethyst ne vous demanderont **jamais** votre clé secrète. - \n- **Faites** une sauvegarde sécurisée de votre clé secrète pour la récupération de votre compte. Nous vous recommandons d'utiliser un gestionnaire de mots de passe. - </string> - <string name="account_backup_tips3_md"> Pour plus de sécurité, vous pouvez chiffrer votre clé avec un mot de passe. Cette clé commence par **ncryptsec1** et ne peut être utilisée sans votre mot de passe. - \n\nSi vous perdez votre mot de passe, vous ne pourrez pas récupérer votre clé. - </string> + <string name="account_backup_tips2_md">## Sauvegarde des Clés et Conseils de Sécurité \n\nVotre compte est sécurisé par une clé secrète. La clé est une longue séquence de caractères commençant par **nsec1**. Toute personne ayant accès à cette clé secrète peut publier et modifier votre identité. \n\n- Ne mettez **pas** votre clé secrète sur un site web ou un logiciel auquel vous ne faites pas confiance. \n- Les développeurs d'Amethyst ne vous demanderont **jamais** votre clé secrète. \n- **Faites** une sauvegarde sécurisée de votre clé secrète pour la récupération de votre compte. Nous vous recommandons d'utiliser un gestionnaire de mots de passe.</string> + <string name="account_backup_tips3_md">Pour plus de sécurité, vous pouvez chiffrer votre clé avec un mot de passe. Cette clé commence par **ncryptsec1** et ne peut être utilisée sans votre mot de passe. \n\nSi vous perdez votre mot de passe, vous ne pourrez pas récupérer votre clé.</string> <string name="encrypt_and_copy_my_secret_key">Chiffrer et copier ma clé secrète</string> <string name="badge_award_image_for">Image de badge pour %1$s</string> <string name="badge_award_image">Image du badge de récompense</string> @@ -905,7 +898,7 @@ <string name="max_subs">Souscription maximale</string> <string name="max_filters_per_sub">Nombre maximum de filtres par sub</string> <string name="max_limit_events_returning">Limite Max (Renvoi des Évènements)</string> - <string name="max_subid_length">Longueur maximale du SubID </string> + <string name="max_subid_length">Longueur maximale du SubID</string> <string name="cashu_mint_label">Mint : %1$s</string> <string name="cashu_redeem">Échanger</string> <string name="cashu_copy_token">Copier le Jeton</string> @@ -1039,9 +1032,7 @@ <string name="push_server_none_explainer">Désactive les Notifications Push</string> <string name="push_server_uses_app_explainer">Utilise l'application %1$s</string> <string name="push_server_install_app">Configuration des Notifications Push</string> - <string name="push_server_install_app_description"> Pour recevoir des notifications push, installez n'importe quelle application qui supporte les [Unified Push](https://unifiedpush.org/), tel que [Ntfy](https://ntfy.sh/). - Après installation, sélectionnez l'application que vous voulez utiliser dans les Paramètres. - </string> + <string name="push_server_install_app_description">Pour recevoir des notifications push, installez n'importe quelle application qui supporte les [Unified Push](https://unifiedpush.org/), tel que [Ntfy](https://ntfy.sh/). Après installation, sélectionnez l'application que vous voulez utiliser dans les Paramètres.</string> <string name="thread_title">Fil de discussion</string> <string name="send_the_seller_a_message">Envoyer un message au vendeur</string> <string name="hi_there_is_this_still_available">Bonjour, est-ce toujours disponible ?</string> @@ -2883,11 +2874,8 @@ <string name="proxy_section">Relais Proxy</string> <string name="proxy_section_explainer">Relais agrégateurs que l'application doit utiliser pour télécharger vos flux, tels que filter.nostr.wine. Cela remplace le modèle de boîte d'envoi et permettra à l'application de se connecter uniquement aux relais de vos listes.</string> <string name="public_chat">Chat public</string> - <string name="public_chat_explainer">Les conversations publiques sont visibles pour tout le monde sur Nostr et n'importe qui - peut y participer. Elles sont idéales pour les communautés ouvertes autour de sujets spécifiques. - La modération peut être contrôlée en supprimant les messages sur ses relais</string> - <string name="public_chat_relays_explainer">Insérez entre 1 et 3 relais qui hébergent ce groupe. - Les clients Nostr utilisent ce paramètre pour savoir où télécharger les messages et où les envoyer.</string> + <string name="public_chat_explainer">Les conversations publiques sont visibles pour tout le monde sur Nostr et n'importe qui peut y participer. Elles sont idéales pour les communautés ouvertes autour de sujets spécifiques. La modération peut être contrôlée en supprimant les messages sur ses relais</string> + <string name="public_chat_relays_explainer">Insérez entre 1 et 3 relais qui hébergent ce groupe. Les clients Nostr utilisent ce paramètre pour savoir où télécharger les messages et où les envoyer.</string> <string name="public_chat_relays_title">Relais</string> <string name="public_chat_title">Métadonnées de tchat public</string> <string name="public_chats">Chats publics</string> @@ -2912,10 +2900,7 @@ <string name="read_only_user">Utilisateur en lecture seule</string> <string name="record_a_message_description">Cliquez et maintenez pour enregistrer un message</string> <string name="relay_chat">Relais de discussion</string> - <string name="relay_chat_explainer">Les relais de discussions sont des groupes de discussion contrôlés par leur relais d'origine. - Ils sont visibles par tous les utilisateurs de Nostr et tout le monde peut y participer. - Ils sont idéaux pour les communautés ouvertes autour de sujets spécifiques. Certains de ces groupes sont éphémères - et, par conséquent, les messages de discussion disparaissent avec le temps</string> + <string name="relay_chat_explainer">Les relais de discussions sont des groupes de discussion contrôlés par leur relais d'origine. Ils sont visibles par tous les utilisateurs de Nostr et tout le monde peut y participer. Ils sont idéaux pour les communautés ouvertes autour de sujets spécifiques. Certains de ces groupes sont éphémères et, par conséquent, les messages de discussion disparaissent avec le temps</string> <string name="relay_chat_title">Relais de discussions</string> <string name="relay_group_flag_private">Privé</string> <string name="relay_management_tab_events">Événements</string> diff --git a/commonsUI/src/commonMain/composeResources/values-fr/strings.xml b/commonsUI/src/commonMain/composeResources/values-fr/strings.xml index 2f9de43271..41b7112fcb 100644 --- a/commonsUI/src/commonMain/composeResources/values-fr/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-fr/strings.xml @@ -211,7 +211,7 @@ <string name="copy_user_id">Copier l'ID utilisateur</string> <string name="unblock_user">Débloquer l'utilisateur</string> <string name="user_is_blocked_hidden">Cet utilisateur est bloqué/muet. Ses publications sont masquées.</string> - <string name="npub_hex_username">npub, hex, nom d'utilisateur </string> + <string name="npub_hex_username">npub, hex, nom d'utilisateur</string> <string name="clear">Nettoyer</string> <string name="app_logo">Logo de l'App</string> <string name="nsec_npub_hex_private_key">nsec / npub / hex private key</string> @@ -259,15 +259,8 @@ <string name="mark_all_known_as_read">Marquer tous les Connus comme lu</string> <string name="mark_all_new_as_read">Marquer tous les Nouveaux comme lu</string> <string name="mark_all_as_read">Tout marquer comme lu</string> - <string name="account_backup_tips2_md"> ## Sauvegarde des Clés et Conseils de Sécurité - \n\nVotre compte est sécurisé par une clé secrète. La clé est une longue séquence de caractères commençant par **nsec1**. Toute personne ayant accès à cette clé secrète peut publier et modifier votre identité. - \n\n- Ne mettez **pas** votre clé secrète sur un site web ou un logiciel auquel vous ne faites pas confiance. - \n- Les développeurs d'Amethyst ne vous demanderont **jamais** votre clé secrète. - \n- **Faites** une sauvegarde sécurisée de votre clé secrète pour la récupération de votre compte. Nous vous recommandons d'utiliser un gestionnaire de mots de passe. - </string> - <string name="account_backup_tips3_md"> Pour plus de sécurité, vous pouvez chiffrer votre clé avec un mot de passe. Cette clé commence par **ncryptsec1** et ne peut être utilisée sans votre mot de passe. - \n\nSi vous perdez votre mot de passe, vous ne pourrez pas récupérer votre clé. - </string> + <string name="account_backup_tips2_md">## Sauvegarde des Clés et Conseils de Sécurité \n\nVotre compte est sécurisé par une clé secrète. La clé est une longue séquence de caractères commençant par **nsec1**. Toute personne ayant accès à cette clé secrète peut publier et modifier votre identité. \n\n- Ne mettez **pas** votre clé secrète sur un site web ou un logiciel auquel vous ne faites pas confiance. \n- Les développeurs d'Amethyst ne vous demanderont **jamais** votre clé secrète. \n- **Faites** une sauvegarde sécurisée de votre clé secrète pour la récupération de votre compte. Nous vous recommandons d'utiliser un gestionnaire de mots de passe.</string> + <string name="account_backup_tips3_md">Pour plus de sécurité, vous pouvez chiffrer votre clé avec un mot de passe. Cette clé commence par **ncryptsec1** et ne peut être utilisée sans votre mot de passe. \n\nSi vous perdez votre mot de passe, vous ne pourrez pas récupérer votre clé.</string> <string name="encrypt_and_copy_my_secret_key">Chiffrer et copier ma clé secrète</string> <string name="badge_award_image_for">Image de badge pour %1$s</string> <string name="badge_award_image">Image du badge de récompense</string> @@ -844,7 +837,7 @@ <string name="max_subs">Souscription maximale</string> <string name="max_filters_per_sub">Nombre maximum de filtres par sub</string> <string name="max_limit_events_returning">Limite Max (Renvoi des Évènements)</string> - <string name="max_subid_length">Longueur maximale du SubID </string> + <string name="max_subid_length">Longueur maximale du SubID</string> <string name="cashu_mint_label">Mint : %1$s</string> <string name="cashu_redeem">Échanger</string> <string name="cashu_copy_token">Copier le Jeton</string> @@ -960,9 +953,7 @@ <string name="push_server_none_explainer">Désactive les Notifications Push</string> <string name="push_server_uses_app_explainer">Utilise l'application %1$s</string> <string name="push_server_install_app">Configuration des Notifications Push</string> - <string name="push_server_install_app_description"> Pour recevoir des notifications push, installez n'importe quelle application qui supporte les [Unified Push](https://unifiedpush.org/), tel que [Ntfy](https://ntfy.sh/). - Après installation, sélectionnez l'application que vous voulez utiliser dans les Paramètres. - </string> + <string name="push_server_install_app_description">Pour recevoir des notifications push, installez n'importe quelle application qui supporte les [Unified Push](https://unifiedpush.org/), tel que [Ntfy](https://ntfy.sh/). Après installation, sélectionnez l'application que vous voulez utiliser dans les Paramètres.</string> <string name="thread_title">Fil de discussion</string> <string name="send_the_seller_a_message">Envoyer un message au vendeur</string> <string name="hi_there_is_this_still_available">Bonjour, est-ce toujours disponible ?</string> @@ -2743,11 +2734,8 @@ <string name="proxy_section">Relais Proxy</string> <string name="proxy_section_explainer">Relais agrégateurs que l'application doit utiliser pour télécharger vos flux, tels que filter.nostr.wine. Cela remplace le modèle de boîte d'envoi et permettra à l'application de se connecter uniquement aux relais de vos listes.</string> <string name="public_chat">Chat public</string> - <string name="public_chat_explainer">Les conversations publiques sont visibles pour tout le monde sur Nostr et n'importe qui - peut y participer. Elles sont idéales pour les communautés ouvertes autour de sujets spécifiques. - La modération peut être contrôlée en supprimant les messages sur ses relais</string> - <string name="public_chat_relays_explainer">Insérez entre 1 et 3 relais qui hébergent ce groupe. - Les clients Nostr utilisent ce paramètre pour savoir où télécharger les messages et où les envoyer.</string> + <string name="public_chat_explainer">Les conversations publiques sont visibles pour tout le monde sur Nostr et n'importe qui peut y participer. Elles sont idéales pour les communautés ouvertes autour de sujets spécifiques. La modération peut être contrôlée en supprimant les messages sur ses relais</string> + <string name="public_chat_relays_explainer">Insérez entre 1 et 3 relais qui hébergent ce groupe. Les clients Nostr utilisent ce paramètre pour savoir où télécharger les messages et où les envoyer.</string> <string name="public_chat_relays_title">Relais</string> <string name="public_chat_title">Métadonnées de tchat public</string> <string name="public_chats">Chats publics</string> @@ -2772,10 +2760,7 @@ <string name="read_only_user">Utilisateur en lecture seule</string> <string name="record_a_message_description">Cliquez et maintenez pour enregistrer un message</string> <string name="relay_chat">Relais de discussion</string> - <string name="relay_chat_explainer">Les relais de discussions sont des groupes de discussion contrôlés par leur relais d'origine. - Ils sont visibles par tous les utilisateurs de Nostr et tout le monde peut y participer. - Ils sont idéaux pour les communautés ouvertes autour de sujets spécifiques. Certains de ces groupes sont éphémères - et, par conséquent, les messages de discussion disparaissent avec le temps</string> + <string name="relay_chat_explainer">Les relais de discussions sont des groupes de discussion contrôlés par leur relais d'origine. Ils sont visibles par tous les utilisateurs de Nostr et tout le monde peut y participer. Ils sont idéaux pour les communautés ouvertes autour de sujets spécifiques. Certains de ces groupes sont éphémères et, par conséquent, les messages de discussion disparaissent avec le temps</string> <string name="relay_chat_title">Relais de discussions</string> <string name="relay_management_tab_events">Événements</string> <string name="relay_management_tab_ips">IPs</string> diff --git a/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml b/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml index 2d16c9c513..cde8a721e1 100644 --- a/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml @@ -646,15 +646,8 @@ <string name="mark_all_known_as_read">सब ज्ञात को पढ लिया चिह्नित करें</string> <string name="mark_all_new_as_read">सब नये को पढ लिया चिह्नित करें</string> <string name="mark_all_as_read">सब को पढ लिया चिह्नित करें</string> - <string name="account_backup_tips2_md">## कुंचिका सुरक्षार्थ अनुकृति तथा सुरक्षा सुझाव - \n\nआपकी लेखा एक गुप्त कुंचिका से सुरक्षित किया गया है। कुंचिका अक्षरों की एक लम्बी श्रृंखला है जो **nsec1** से आरम्भ होता है। जिस किसी के पास यह कुंचिका है वह पत्र प्रकाशित कर सकेगा तथा आपका परिचय परिवर्तित कर सकेगा। - \n\n- आपकी गुप्त कुंचिका किसी भी जालस्थान अथवा क्रमक में **ना** डालें जिसपर आपका विश्वास ना हो। - \n- अमेथिस्ट क्रमलेख विकासकर्ता आपकी गुप्त कुंचिका **कभी नहीं** पूछेंगे। - \n- आपकी गुप्त कुंचिका की एक सुरक्षित अनुकृति **अवश्य** रखें अपनी लेखा पुनः प्राप्त करने के लिए। पारणशब्ध प्रबंधन क्रमक की पुनः प्रशंसा करते हैं। - </string> - <string name="account_backup_tips3_md"> अधिक सुरक्षा के लिए आप अपनी कुंचिका का रहस्यीकरण कर सकते हैं एक पारणशब्द के साथ। यह कुंचिका आरम्भ होगी **ncryptsec1** से तथा पारणशब्द के बिना प्रयोग साध्य नहीं। - \n\n यदि आप अपना पारणशब्द खो देते हैं, तो आप अपनी कुंचिका को पुनः प्राप्त नहीं कर सकते। - </string> + <string name="account_backup_tips2_md">## कुंचिका सुरक्षार्थ अनुकृति तथा सुरक्षा सुझाव \n\nआपकी लेखा एक गुप्त कुंचिका से सुरक्षित किया गया है। कुंचिका अक्षरों की एक लम्बी श्रृंखला है जो **nsec1** से आरम्भ होता है। जिस किसी के पास यह कुंचिका है वह पत्र प्रकाशित कर सकेगा तथा आपका परिचय परिवर्तित कर सकेगा। \n\n- आपकी गुप्त कुंचिका किसी भी जालस्थान अथवा क्रमक में **ना** डालें जिसपर आपका विश्वास ना हो। \n- अमेथिस्ट क्रमलेख विकासकर्ता आपकी गुप्त कुंचिका **कभी नहीं** पूछेंगे। \n- आपकी गुप्त कुंचिका की एक सुरक्षित अनुकृति **अवश्य** रखें अपनी लेखा पुनः प्राप्त करने के लिए। पारणशब्ध प्रबंधन क्रमक की पुनः प्रशंसा करते हैं।</string> + <string name="account_backup_tips3_md">अधिक सुरक्षा के लिए आप अपनी कुंचिका का रहस्यीकरण कर सकते हैं एक पारणशब्द के साथ। यह कुंचिका आरम्भ होगी **ncryptsec1** से तथा पारणशब्द के बिना प्रयोग साध्य नहीं। \n\n यदि आप अपना पारणशब्द खो देते हैं, तो आप अपनी कुंचिका को पुनः प्राप्त नहीं कर सकते।</string> <string name="backup_keys_nudge_title">अपने कुंचिकाओं को सुरक्षित रखें</string> <string name="backup_keys_nudge_body">आपकी गुप्त कुंचिका एकमात्र विधि है इस लेखा का अभिगमन करने के लिए। यदि आप इसे खो देते हैं तो इसे कभी भी पुनःप्राप्त नहीं कर सकते। इसे कहीं सुरक्षित रखें अभी इस समय।</string> <string name="backup_keys_nudge_backup_now">अभी इसी समय सुरक्षित अनुकृति बनाएँ</string> @@ -1834,9 +1827,7 @@ <string name="push_server_none_explainer">प्रेषित सूचनाएँ अक्षम करता है</string> <string name="push_server_uses_app_explainer">%1$s क्रमक का प्रयोग करता है</string> <string name="push_server_install_app">प्रेषित सूचना स्थापन</string> - <string name="push_server_install_app_description"> प्रेषित सूचनाएँ प्राप्त करने के लिए, किसी भी क्रमक की स्थापना करें जो [संयुक्त प्रेषण](https://unifiedpush.org/) का अवलम्बन करता है, जैसे [एनटीफै](https://ntfy.sh/)। - स्थापना पश्चात, जिस क्रमक का उपयोग करना चाहते हैं उसका चयन करें स्थापना विकल्पों में। - </string> + <string name="push_server_install_app_description">प्रेषित सूचनाएँ प्राप्त करने के लिए, किसी भी क्रमक की स्थापना करें जो [संयुक्त प्रेषण](https://unifiedpush.org/) का अवलम्बन करता है, जैसे [एनटीफै](https://ntfy.sh/)। स्थापना पश्चात, जिस क्रमक का उपयोग करना चाहते हैं उसका चयन करें स्थापना विकल्पों में।</string> <string name="thread_title">सूत्र</string> <string name="send_the_seller_a_message">विक्रेता को संदेश भेजें</string> <string name="hi_there_is_this_still_available">नमस्ते, क्या यह अब भी उपलब्ध है?</string> @@ -3585,17 +3576,9 @@ <string name="could_not_fetch_invoice_from_details">चालान नहीं लाया जा सका %1$s से : %2$s</string> <string name="could_not_open_the_compressed_file">संकुचित अभिलेख को खोल नहीं पाए</string> <string name="couldnt_find_nwc_wallets">धनकोष अप्राप्त</string> - <string name="couldnt_find_nwc_wallets_description">अमेथिस्ट को कोई धनकोष क्रमक प्राप्य नहीं जो नोस्टर धनकोष संयोजन का अवलम्बन करता हो। -\n\nआप यह कर सकते हैं : -\n\t१॰ नोस्टर धनकोष संयोजन अवलम्बन का सत्यापन करें। दुबारा जाँचें की आपका मनचाहा धनकोष क्रमक नोस्टर धनकोष संयोजन के साथ संगत है। -\n\t२॰ संयोजन बनाएँ। आपके धनकोष क्रमक में जाकर एक नया नोस्टर धनकोष संयोजन बनाएँ। -\n\t३॰ अमेथिस्ट के साथ जोडें। आपका धनकोष क्रमक सामान्यतः तीन विकल्प देगा। -\n\t\t३॰१ स्वचालित जोड। एक घुण्डी जिससे स्वचालित रूप से खोलें तथा अमेथिस्ट से जोडें। यदि हो तो उस घुण्डी को टाँकें तथा अमेथिस्ट खोलें तथा नव्य पटल पर अभिलेखन करें। -\n\t\t३॰२ मनुष्यकृत जोड। एक संयोजन वैश्विकसंसाधनपरिचायक जिसकी अनुकृति करके सीधे इस पटल पर प्रविष्ट कर सकते हैं। -\n\t\t३॰३ क्यूआर॰ चित्र अक्षरराशि। धनकोष क्रमक एक क्यूआर॰ चित्र अक्षरराशि बनएगा जिसे आप परख सकते हैं इस पटल पर क्यूआर॰ क्रमचित्र घुण्डी द्वारा। - </string> + <string name="couldnt_find_nwc_wallets_description">अमेथिस्ट को कोई धनकोष क्रमक प्राप्य नहीं जो नोस्टर धनकोष संयोजन का अवलम्बन करता हो। \n\nआप यह कर सकते हैं : \n\t१॰ नोस्टर धनकोष संयोजन अवलम्बन का सत्यापन करें। दुबारा जाँचें की आपका मनचाहा धनकोष क्रमक नोस्टर धनकोष संयोजन के साथ संगत है। \n\t२॰ संयोजन बनाएँ। आपके धनकोष क्रमक में जाकर एक नया नोस्टर धनकोष संयोजन बनाएँ। \n\t३॰ अमेथिस्ट के साथ जोडें। आपका धनकोष क्रमक सामान्यतः तीन विकल्प देगा। \n\t\t३॰१ स्वचालित जोड। एक घुण्डी जिससे स्वचालित रूप से खोलें तथा अमेथिस्ट से जोडें। यदि हो तो उस घुण्डी को टाँकें तथा अमेथिस्ट खोलें तथा नव्य पटल पर अभिलेखन करें। \n\t\t३॰२ मनुष्यकृत जोड। एक संयोजन वैश्विकसंसाधनपरिचायक जिसकी अनुकृति करके सीधे इस पटल पर प्रविष्ट कर सकते हैं। \n\t\t३॰३ क्यूआर॰ चित्र अक्षरराशि। धनकोष क्रमक एक क्यूआर॰ चित्र अक्षरराशि बनएगा जिसे आप परख सकते हैं इस पटल पर क्यूआर॰ क्रमचित्र घुण्डी द्वारा।</string> <string name="create">बनाएँ</string> - <string name="d"> दि॰</string> + <string name="d">दि॰</string> <string name="danger_zone">संकट क्षेत्र</string> <string name="dark">अन्धकारमय</string> <string name="description">विवरण</string> @@ -3791,7 +3774,7 @@ <string name="git_repositories_search_close">छलनी आवृत</string> <string name="git_repositories_search_open">क्रमलेखकोश छलनी</string> <string name="goal_progress">%1$s वित्तपोषित %2$s साट्स उद्देश्य में से</string> - <string name="h"> घं॰</string> + <string name="h">घं॰</string> <string name="hash_verification_failed">यह विषयवस्तु परिवर्तित हुआ है। हो सकता है लेखक ने परिवर्तन देखा नहीं अथवा अनुमति दिया नहीं।</string> <string name="hash_verification_info_title">इसका अर्थ क्या है?</string> <string name="hash_verification_passed">यह विषयवस्तु वैसे ही है जैसे पत्र प्रकाशन पर</string> @@ -4080,7 +4063,7 @@ <string name="login_with_a_private_key_to_be_able_to_upload">आप ख्याप्य कुंचिका का प्रयोग कर रहे हैं जिससे केवल पढ सकते हैं। आरोहण करने के लिए गुप्त कुंचिका के साथ प्रवेशांकन करें</string> <string name="login_with_a_private_key_to_like_posts">आप ख्याप्य कुंचिका का प्रयोग कर रहे हैं जिससे केवल पढ सकते हैं। प्रकाशित पत्रों को चाहने के लिए गुप्त कुंचिका के साथ प्रवेशांकन करें</string> <string name="longs">चलचित्र</string> - <string name="m"> मि॰</string> + <string name="m">मि॰</string> <string name="manage_emoji_packs">मेरे भावचिह्न पोटलियाँ</string> <string name="marmot_add_photo">चित्र जोडें</string> <string name="marmot_adding_user">जोडा जा रहा है %1$s…</string> @@ -4711,11 +4694,8 @@ <string name="proxy_section">प्रतिनिधि पुनःप्रसारक</string> <string name="proxy_section_explainer">संग्रहकर्ता पुनःप्रसारक जो क्रमक द्वारा उपयुक्त होना चाहिए आपके सूचनावलियों का अवरोहण करने के लिए। जैसे कि filter.nostr.wine । यह निर्गतपेटिका प्रतिरूप को प्रतिस्थापित करेगा तथा क्रमक आपके सूचियों में पुनःप्रसारकों के साथ ही जुडेगा।</string> <string name="public_chat">सार्वजनिक चर्चा</string> - <string name="public_chat_explainer">सार्वजनिक चर्चाएँ सबके लिए दृश्यमान हैं नोस्टर पर तथा सभी - इनमें भाग ले सकते हैं। ये उत्कृष्ट हैं खुले समुदायों के लिए विशिष्ट विषयों के प्रसंग में। - नियमन का नियन्त्रण किया जा सकता है पत्रों को मिटाने से पुनःप्रसारकों में</string> - <string name="public_chat_relays_explainer">इस समुदाय के निवास के लिए १ - ३ पुनःप्रसारकों को जोडें। - नोस्टर ग्राहक इस स्थापना विकल्प का प्रयोग करेंगे यह जानने के लिए कि कहाँ से सन्देशों का अवरोहण करना है तथा कहाँ आपके सन्देश भेजने हैं।</string> + <string name="public_chat_explainer">सार्वजनिक चर्चाएँ सबके लिए दृश्यमान हैं नोस्टर पर तथा सभी इनमें भाग ले सकते हैं। ये उत्कृष्ट हैं खुले समुदायों के लिए विशिष्ट विषयों के प्रसंग में। नियमन का नियन्त्रण किया जा सकता है पत्रों को मिटाने से पुनःप्रसारकों में</string> + <string name="public_chat_relays_explainer">इस समुदाय के निवास के लिए १ - ३ पुनःप्रसारकों को जोडें। नोस्टर ग्राहक इस स्थापना विकल्प का प्रयोग करेंगे यह जानने के लिए कि कहाँ से सन्देशों का अवरोहण करना है तथा कहाँ आपके सन्देश भेजने हैं।</string> <string name="public_chat_relays_title">पुनःप्रसारक</string> <string name="public_chat_title">सार्वजनिक चर्चा उपतथ्य</string> <string name="public_chats">सार्वजनिक चर्चा</string> @@ -4780,10 +4760,7 @@ <string name="relay_auth_search_keywords">प्रमाण प्रमाणीकरण प्रवेशांकन पुनःप्रसारक हस्ताक्षरण सत्यापन निप॰४२ परिचय</string> <string name="relay_auth_settings_title">पुनःप्रसारक प्रमाणीकरण</string> <string name="relay_chat">पुनःप्रसारक चर्चा</string> - <string name="relay_chat_explainer">पुनःप्रसारक चर्चाएँ आवासी पुनःप्रसारक द्वारा नियन्त्रित चर्चा समुदाएँ हैं। - वे नोस्टर पर सभी के लिए दृश्य हैं तथा उनमें सभी भाग ले सकते हैं। - वे उत्कृष्ट हैं खुले समुदायों के लिए विशिष्ट विषयों पर। इनमें से कुछ समुदाएँ अस्थायी हैं। - तथा इसीलिए समय के साथ चर्चा सन्देश अदृश्य हो जाते हैं</string> + <string name="relay_chat_explainer">पुनःप्रसारक चर्चाएँ आवासी पुनःप्रसारक द्वारा नियन्त्रित चर्चा समुदाएँ हैं। वे नोस्टर पर सभी के लिए दृश्य हैं तथा उनमें सभी भाग ले सकते हैं। वे उत्कृष्ट हैं खुले समुदायों के लिए विशिष्ट विषयों पर। इनमें से कुछ समुदाएँ अस्थायी हैं। तथा इसीलिए समय के साथ चर्चा सन्देश अदृश्य हो जाते हैं</string> <string name="relay_chat_title">पुनःप्रसारक चर्चाएँ</string> <string name="relay_explain_account_data">आपके अपने परिचय तथा स्थापना विकल्प तथा पाण्डुलिपियाँ। आपके मुख्य पुनःप्रसारकों पर।</string> <string name="relay_explain_community_chats">पुनःप्रसारक जिनपर प्रत्येक समुदाय अपने समतलों को प्रकाशित करते हैं।</string> diff --git a/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml b/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml index 4931f70f61..f7872f3f26 100644 --- a/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml @@ -311,7 +311,7 @@ <string name="podcast_value_split_percent">%1$d%%</string> <string name="podcast_value_for_value">Értéket az értékért</string> <string name="relay_monitor_rtt_open">Megnyitás</string> - <string name="relay_monitor_rtt_read">Olvasás </string> + <string name="relay_monitor_rtt_read">Olvasás</string> <string name="relay_monitor_rtt_write">Írás</string> <string name="relay_monitor_network">Hálózat</string> <string name="relay_monitor_relay_type">Típus</string> @@ -461,8 +461,7 @@ <string name="send_payment_receipt_section">Zap-igazolás</string> <string name="send_payment_receipt_clink">Közvetlen lightning-fizetés - nem lesz zapigazolás közzétéve a Nostr-on.</string> <string name="send_payment_receipt_onchain">Egy láncon belüli zapigazolás közzé lesz téve a Nostr-on, így a címzett megtalálhatja a kifizetést.</string> - <string name="send_payment_receipt_onchain_address">Fizetés közvetlenül a láncon belüli pénztárcából a profil megadott bitcoin-címére (%1$s) - nem lesz zapigazolás közzétéve. -</string> + <string name="send_payment_receipt_onchain_address">Fizetés közvetlenül a láncon belüli pénztárcából a profil megadott bitcoin-címére (%1$s) - nem lesz zapigazolás közzétéve.</string> <string name="send_payment_receipt_cashu">A nutzap-esemény magát az ecasht kézbesíti, így az mindig közzé lesz téve.</string> <string name="send_payment_requesting_invoice">Számla kérése…</string> <string name="send_payment_requesting_invoice_nostr">Számla kérése a Nostr hálózaton keresztül…</string> @@ -647,15 +646,8 @@ <string name="mark_all_known_as_read">Az összes ismert megjelölése olvasottként</string> <string name="mark_all_new_as_read">Az összes új üzenet megjelölése olvasottként</string> <string name="mark_all_as_read">Összes megjelölése olvasottként</string> - <string name="account_backup_tips2_md"> ## Kulcs- és biztonsági mentési tippek - \n\nFiókját egy titkos kulcs védi. A kulcs egy hosszú véletlenszerű karakterlánc, amely **nsec1**-vel kezdődik. Bárki, aki az Ön titkos kulcsához hozzáfér, az Ön személyazonosságának használatával bármilyen tartalmat közzétehet. - \n\n- **Ne** helyezze el a titkos kulcsát olyan webhelyen vagy szoftverben, amelyben nem bízik. - \n- Az Amethyst fejlesztői az Ön titkos kulcsát **soha** nem fogják Öntől elkérni. - \n- A fiók-helyreállításának érdekében, a titkos kulcsáról **mindig** készítsen biztonsági másolatot. Javasoljuk egy jelszókezelő használatát. - </string> - <string name="account_backup_tips3_md"> A nagyobb biztonság érdekében titkosítsa egy jelszóval. A jelszóval védett kulcs **ncryptsec1**-vel kezdődik, és a jelszava nélkül nem használható. - \n\nHa elveszíti a jelszavát, nem tudja visszaállítani a kulcsát. - </string> + <string name="account_backup_tips2_md">## Kulcs- és biztonsági mentési tippek \n\nFiókját egy titkos kulcs védi. A kulcs egy hosszú véletlenszerű karakterlánc, amely **nsec1**-vel kezdődik. Bárki, aki az Ön titkos kulcsához hozzáfér, az Ön személyazonosságának használatával bármilyen tartalmat közzétehet. \n\n- **Ne** helyezze el a titkos kulcsát olyan webhelyen vagy szoftverben, amelyben nem bízik. \n- Az Amethyst fejlesztői az Ön titkos kulcsát **soha** nem fogják Öntől elkérni. \n- A fiók-helyreállításának érdekében, a titkos kulcsáról **mindig** készítsen biztonsági másolatot. Javasoljuk egy jelszókezelő használatát.</string> + <string name="account_backup_tips3_md">A nagyobb biztonság érdekében titkosítsa egy jelszóval. A jelszóval védett kulcs **ncryptsec1**-vel kezdődik, és a jelszava nélkül nem használható. \n\nHa elveszíti a jelszavát, nem tudja visszaállítani a kulcsát.</string> <string name="backup_keys_nudge_title">Biztonsági mentés készítése a kulcsokról</string> <string name="backup_keys_nudge_body">A titkos kulcs megadása az egyetlen módja a fiók elérésének. Ha elveszíti, akkor soha nem tudja visszaszerezni. Mentse el biztonságos helyre most.</string> <string name="backup_keys_nudge_backup_now">Biztonsági mentés most</string> @@ -779,7 +771,7 @@ <string name="award_badge_remove_recipient">Eltávolítás</string> <string name="badge_untitled">Cím nélküli kitűző</string> <string name="badge_awardees_label">A kitűzőt %1$d érdemelte ki</string> - <string name="profile_badges_header">Kitűzők · %1$d </string> + <string name="profile_badges_header">Kitűzők · %1$d</string> <string name="profile_badges_description">Válassza ki, hogy a megszerzett kitűzők közül melyek jelenjenek meg a profilban.</string> <string name="profile_badges_empty">Ön még nem kapott kitűzőt.</string> <string name="profile_apps_header">Alkalmazások · %1$d</string> @@ -1835,9 +1827,7 @@ <string name="push_server_none_explainer">Push-értesítések letiltása</string> <string name="push_server_uses_app_explainer">%1$s alkalmazást használ</string> <string name="push_server_install_app">Push-értesítések beállítása</string> - <string name="push_server_install_app_description"> A push-értesítések fogadásához olyan alkalmazást telepítsen, amely támogatja a [Unified Push](https://unifiedpush.org/), például az [Ntfy](https://ntfy.sh/) alkalmazást. - A telepítés után a beállításokban válassza ki a használni kívánt alkalmazást. - </string> + <string name="push_server_install_app_description">A push-értesítések fogadásához olyan alkalmazást telepítsen, amely támogatja a [Unified Push](https://unifiedpush.org/), például az [Ntfy](https://ntfy.sh/) alkalmazást. A telepítés után a beállításokban válassza ki a használni kívánt alkalmazást.</string> <string name="thread_title">Bejegyzés</string> <string name="send_the_seller_a_message">Üzenet küldése az eladónak</string> <string name="hi_there_is_this_still_available">Tisztelt hölgyem / uram! Ez még aktuális?</string> @@ -3124,7 +3114,7 @@ <string name="app_notification_calls_channel_description">Értesítések a bejövő hang- és videóhívásokhoz</string> <string name="app_notification_calls_channel_name">Bejövő hívások</string> <string name="app_notification_chess_challenge_accepted">%1$s elfogadta az Ön sakk-kihívását</string> - <string name="app_notification_chess_channel_description">Értesítés a sakkjátékkal kapcsolatos eseményekre </string> + <string name="app_notification_chess_channel_description">Értesítés a sakkjátékkal kapcsolatos eseményekre</string> <string name="app_notification_chess_channel_name">Sakk</string> <string name="app_notification_chess_summary">Sakkfrissítések</string> <string name="app_notification_chess_your_turn">%1$s lépett - Ön következik</string> @@ -3586,15 +3576,7 @@ <string name="could_not_fetch_invoice_from_details">Nem sikerült lekérni a számlát a következőtől: %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Nem sikerült megnyitni a tömörített fájlt</string> <string name="couldnt_find_nwc_wallets">Nem található pénztárca</string> - <string name="couldnt_find_nwc_wallets_description">Az Amethyst nem talált olyan pénztárca-alkalmazást, amely támogatja a Nostr Wallet Connect (NWC) szolgáltatást. -\n\nAmit tehet: -\n\t1. NWC-támogatás ellenőrzése: Ellenőrizze, hogy a kívánt pénztárca-alkalmazás kompatibilis-e a Nostr Wallet Connect szolgáltatással. -\n\t2. Kapcsolat létrehozása: Nyissa meg a pénztárcaalkalmazást, és hozzon létre egy új NWC-kapcsolatot. -\n\t3. Kapcsolódás az Amethysthez: A pénztárcája általában három lehetőséget kínál: -\n\t\t3.1 Automatikus kapcsolódás: Egy gomb az Amethyst automatikus megnyitásához és kapcsolódáshoz. Koppintson erre a gombra, nyissa meg az Amethyst alkalmazást, és mentse el az új képernyőn. -\n\t\t3.2 Kézi kapcsolódás: Egy kapcsolódási cím, amelyet kimásolhat, és a képernyőn található beillesztés gombra koppintva közvetlenül beírhatja. -\n\t\t3.3 QR-kód: A pénztárca létrehoz egy QR-kódot, amelyet a képernyőn található QR-kód gombról olvashat be. - </string> + <string name="couldnt_find_nwc_wallets_description">Az Amethyst nem talált olyan pénztárca-alkalmazást, amely támogatja a Nostr Wallet Connect (NWC) szolgáltatást. \n\nAmit tehet: \n\t1. NWC-támogatás ellenőrzése: Ellenőrizze, hogy a kívánt pénztárca-alkalmazás kompatibilis-e a Nostr Wallet Connect szolgáltatással. \n\t2. Kapcsolat létrehozása: Nyissa meg a pénztárcaalkalmazást, és hozzon létre egy új NWC-kapcsolatot. \n\t3. Kapcsolódás az Amethysthez: A pénztárcája általában három lehetőséget kínál: \n\t\t3.1 Automatikus kapcsolódás: Egy gomb az Amethyst automatikus megnyitásához és kapcsolódáshoz. Koppintson erre a gombra, nyissa meg az Amethyst alkalmazást, és mentse el az új képernyőn. \n\t\t3.2 Kézi kapcsolódás: Egy kapcsolódási cím, amelyet kimásolhat, és a képernyőn található beillesztés gombra koppintva közvetlenül beírhatja. \n\t\t3.3 QR-kód: A pénztárca létrehoz egy QR-kódot, amelyet a képernyőn található QR-kód gombról olvashat be.</string> <string name="create">Létrehozás</string> <string name="d">n</string> <string name="danger_zone">Kritikus beállítások</string> @@ -3884,7 +3866,7 @@ <string name="kind_adventure_reading">Kalandos olvasmány</string> <string name="kind_adventure_scene">Kalandjelenet</string> <string name="kind_app_curation_set">Alkalmazáslista</string> - <string name="kind_app_recommendations">Ajánlott alkalmazások </string> + <string name="kind_app_recommendations">Ajánlott alkalmazások</string> <string name="kind_appointment">Találkozó</string> <string name="kind_apps">Alkalmazások</string> <string name="kind_appt_rsvp">Időpont-visszaigazolás</string> @@ -4712,11 +4694,8 @@ <string name="proxy_section">Proxyzott átjátszók</string> <string name="proxy_section_explainer">Összesítő átjátszók, amelyeket az alkalmazásnak használnia kell a hírfolyamok letöltéséhez, például filter.nostr.wine. Ez helyettesíti a kimeneti modellt, és az alkalmazás csak a listákban szereplő átjátszókhoz fog csatlakozni.</string> <string name="public_chat">Nyilvános csevegés</string> - <string name="public_chat_explainer">A nyilvános csevegések mindenki számára láthatóak a Nostr-on, és bárki - részt vehet bennük. Ezek kiválóan alkalmasak bizonyos témák köré szerveződő nyílt közösségek számára. - A moderálás az átjátszókon lévő hozzászólások törlésével szabályozható</string> - <string name="public_chat_relays_explainer">Adjon hozzá 1–3 olyan átjátszót, amelyek kiszolgálják ezt a csoportot. - A Nostr-kliensek ezt a beállítást használják arra, hogy tudják, honnan töltsék le az üzeneteket, és hová küldjék az üzeneteket.</string> + <string name="public_chat_explainer">A nyilvános csevegések mindenki számára láthatóak a Nostr-on, és bárki részt vehet bennük. Ezek kiválóan alkalmasak bizonyos témák köré szerveződő nyílt közösségek számára. A moderálás az átjátszókon lévő hozzászólások törlésével szabályozható</string> + <string name="public_chat_relays_explainer">Adjon hozzá 1–3 olyan átjátszót, amelyek kiszolgálják ezt a csoportot. A Nostr-kliensek ezt a beállítást használják arra, hogy tudják, honnan töltsék le az üzeneteket, és hová küldjék az üzeneteket.</string> <string name="public_chat_relays_title">Átjátszók</string> <string name="public_chat_title">Nyilvános csevegés metaadatai</string> <string name="public_chats">Nyilvános csevegések</string> @@ -4781,10 +4760,7 @@ <string name="relay_auth_search_keywords">hitelesítés, átjátszó, aláírás, ellenőrzés, nip-42, azonosság</string> <string name="relay_auth_settings_title">Átjátszó-hitelesítés</string> <string name="relay_chat">Átjátszócsevegés</string> - <string name="relay_chat_explainer">Az átjátszócsevegések olyan csevegőcsoportok, amelyeket az otthoni átjátszó irányít. - Ezek mindenki számára láthatóak a Nostr-on, és bárki részt vehet rajtuk. - Remekül alkalmasak nyitott közösségek kialakítására bizonyos témák körül. Néhány ilyen csoport - rövid élettartamú, ezért a csevegési üzenetek idővel eltűnnek</string> + <string name="relay_chat_explainer">Az átjátszócsevegések olyan csevegőcsoportok, amelyeket az otthoni átjátszó irányít. Ezek mindenki számára láthatóak a Nostr-on, és bárki részt vehet rajtuk. Remekül alkalmasak nyitott közösségek kialakítására bizonyos témák körül. Néhány ilyen csoport rövid élettartamú, ezért a csevegési üzenetek idővel eltűnnek</string> <string name="relay_chat_title">Átjátszócsevegések</string> <string name="relay_explain_account_data">Saját profil, -beállítások és -piszkozatok a gazda-átjátszókon.</string> <string name="relay_explain_community_chats">Azok az átjátszók, amelyekre az egyes közösségek közzéteszik a terveiket.</string> diff --git a/commonsUI/src/commonMain/composeResources/values-in-rID/strings.xml b/commonsUI/src/commonMain/composeResources/values-in-rID/strings.xml index 7b53a2b3e2..3ce5510f41 100644 --- a/commonsUI/src/commonMain/composeResources/values-in-rID/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-in-rID/strings.xml @@ -266,15 +266,8 @@ <string name="mark_all_known_as_read">Tandai semua yang dikenal sebagai telah dibaca</string> <string name="mark_all_new_as_read">Tandai semua yang baru sebagai telah dibaca</string> <string name="mark_all_as_read">Tandai semua sebagai telah dibaca</string> - <string name="account_backup_tips2_md"> ## Tips Cadangan Kunci dan Keamanan - \n\nAkun Anda diamankan dengan kunci rahasia. Kunci ini adalah rangkaian karakter panjang yang diawali dengan **nsec1**. Siapa pun yang memiliki akses ke kunci rahasia ini dapat memposting dan mengubah identitas Anda. - \n\n- **Jangan** memasukkan kunci rahasia Anda ke situs web atau perangkat lunak yang tidak Anda percaya. - \n- Pengembang Amethyst **tidak akan pernah** meminta kunci rahasia Anda. - \n- **Lakukan** pencadangan kunci rahasia Anda secara aman untuk pemulihan akun. Kami merekomendasikan penggunaan pengelola kata sandi. - </string> - <string name="account_backup_tips3_md"> Untuk keamanan tambahan, Anda dapat mengenkripsi kunci dengan kata sandi. Kunci ini diawali dengan **ncryptsec1** dan tidak dapat digunakan tanpa kata sandi Anda. - \n\nJika Anda kehilangan kata sandi, Anda tidak akan dapat memulihkan kunci Anda. - </string> + <string name="account_backup_tips2_md">## Tips Cadangan Kunci dan Keamanan \n\nAkun Anda diamankan dengan kunci rahasia. Kunci ini adalah rangkaian karakter panjang yang diawali dengan **nsec1**. Siapa pun yang memiliki akses ke kunci rahasia ini dapat memposting dan mengubah identitas Anda. \n\n- **Jangan** memasukkan kunci rahasia Anda ke situs web atau perangkat lunak yang tidak Anda percaya. \n- Pengembang Amethyst **tidak akan pernah** meminta kunci rahasia Anda. \n- **Lakukan** pencadangan kunci rahasia Anda secara aman untuk pemulihan akun. Kami merekomendasikan penggunaan pengelola kata sandi.</string> + <string name="account_backup_tips3_md">Untuk keamanan tambahan, Anda dapat mengenkripsi kunci dengan kata sandi. Kunci ini diawali dengan **ncryptsec1** dan tidak dapat digunakan tanpa kata sandi Anda. \n\nJika Anda kehilangan kata sandi, Anda tidak akan dapat memulihkan kunci Anda.</string> <string name="encrypt_and_copy_my_secret_key">Enkripsi dan salin kunci rahasia saya</string> <string name="badge_award_image_for">Gambar penghargaan lencana untuk %1$s</string> <string name="badge_award_image">"Gambar penghargaan lencana</string> @@ -853,13 +846,11 @@ <string name="meeting_space_closed_tag">DITUTUP</string> <string name="meeting_space_planned_tag">TERJADWAL</string> <string name="meeting_space_planned_starts_at">Mulai %1$s</string> - <string name="are_you_sure_you_want_to_log_out">Keluar akan menghapus semua informasi yang disimpan secara lokal. -Pastikan kunci pribadi Anda telah simpan untuk menghindari kehilangan akun Anda. Apakah Anda ingin melanjutkan?</string> + <string name="are_you_sure_you_want_to_log_out">Keluar akan menghapus semua informasi yang disimpan secara lokal. Pastikan kunci pribadi Anda telah simpan untuk menghindari kehilangan akun Anda. Apakah Anda ingin melanjutkan?</string> <string name="followed_tags">Tags yang Diikuti</string> <string name="discover_follows_explainer">Ini adalah daftar pengguna yang Anda rekomendasikan kepada orang lain. Hanya pengguna publik yang diizinkan.</string> <string name="community_approved_posts">Post yang disetujui</string> - <string name="groups_no_descriptor">Grup ini tidak memiliki deskripsi atau peraturan. -Beri tahu kepada pemilik untuk menambahnya</string> + <string name="groups_no_descriptor">Grup ini tidak memiliki deskripsi atau peraturan. Beri tahu kepada pemilik untuk menambahnya</string> <string name="community_no_descriptor">Komunitas ini tidak memiliki deskripsi. Beri tahu kepada pemilik untuk menambahnya</string> <string name="user_preferences">Preferensi Pengguna</string> <string name="settings_search_placeholder">Cari pengaturan</string> @@ -951,9 +942,7 @@ Beri tahu kepada pemilik untuk menambahnya</string> <string name="push_server_none_explainer">Matikan Pemberitahuan Push</string> <string name="push_server_uses_app_explainer">Memakai aplikasi %1$s</string> <string name="push_server_install_app">Pengaturan Pemberitahuan Push</string> - <string name="push_server_install_app_description"> Untuk menerima Pemberitahuan Push, Install aplikasi yang mendukung [Unified Push](https://unifiedpush.org/), seperti [Ntfy](https://ntfy.sh/). - Setelah diinstall, Pilih aplikasi yang ingin digunakan di pengaturan. - </string> + <string name="push_server_install_app_description">Untuk menerima Pemberitahuan Push, Install aplikasi yang mendukung [Unified Push](https://unifiedpush.org/), seperti [Ntfy](https://ntfy.sh/). Setelah diinstall, Pilih aplikasi yang ingin digunakan di pengaturan.</string> <string name="thread_title">Utas</string> <string name="send_the_seller_a_message">Kirim pesan ke penjual</string> <string name="hi_there_is_this_still_available">Halo, apakah ini masih tersedia?</string> @@ -1991,15 +1980,7 @@ Beri tahu kepada pemilik untuk menambahnya</string> <string name="could_not_fetch_invoice_from_details">Tidak dapat mengambil invoice dari %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Tidak dapat membuka file terkompresi</string> <string name="couldnt_find_nwc_wallets">Dompet tidak ditemukan</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst tidak dapat menemukan aplikasi dompet apa pun yang mendukung Nostr Wallet Connect (NWC). -\n\nYang dapat Anda lakukan: -\n\t1. Verifikasi Dukungan NWC: Pastikan aplikasi dompet pilihan Anda kompatibel dengan Nostr Wallet Connect. -\n\t2. Buat Koneksi: Buka aplikasi dompet Anda dan buat koneksi NWC baru. -\n\t3. Hubungkan ke Amethyst: Dompet Anda biasanya akan memberikan tiga pilihan: -\n\t\t3.1 Tautan Otomatis: Tombol untuk secara otomatis membuka dan terhubung ke Amethyst. Jika ada, klik tombol tersebut, buka Amethyst dan tekan simpan di layar baru. -\n\t\t3.2 Tautan Manual: URI Koneksi yang dapat Anda salin dan klik ikon tempel di layar ini untuk memasukkannya secara langsung. -\n\t\t3.3 Kode QR: Dompet membuat kode QR yang dapat Anda baca dari tombol kode QR di layar ini. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst tidak dapat menemukan aplikasi dompet apa pun yang mendukung Nostr Wallet Connect (NWC). \n\nYang dapat Anda lakukan: \n\t1. Verifikasi Dukungan NWC: Pastikan aplikasi dompet pilihan Anda kompatibel dengan Nostr Wallet Connect. \n\t2. Buat Koneksi: Buka aplikasi dompet Anda dan buat koneksi NWC baru. \n\t3. Hubungkan ke Amethyst: Dompet Anda biasanya akan memberikan tiga pilihan: \n\t\t3.1 Tautan Otomatis: Tombol untuk secara otomatis membuka dan terhubung ke Amethyst. Jika ada, klik tombol tersebut, buka Amethyst dan tekan simpan di layar baru. \n\t\t3.2 Tautan Manual: URI Koneksi yang dapat Anda salin dan klik ikon tempel di layar ini untuk memasukkannya secara langsung. \n\t\t3.3 Kode QR: Dompet membuat kode QR yang dapat Anda baca dari tombol kode QR di layar ini.</string> <string name="create">Buat</string> <string name="d">h</string> <string name="danger_zone">Zona Berbahaya</string> @@ -2183,8 +2164,7 @@ Beri tahu kepada pemilik untuk menambahnya</string> <string name="image_saved_to_the_gallery">Gambar disimpan ke galeri</string> <string name="impersonation">Identitas palsu</string> <string name="inbox_relays_title">Relay Kotak Masuk</string> - <string name="incorrect_invoice_amount_sats_from_it_should_have_been">Jumlah faktur salah (%1$s sat) dari %2$s. -Seharusnya %3$s</string> + <string name="incorrect_invoice_amount_sats_from_it_should_have_been">Jumlah faktur salah (%1$s sat) dari %2$s. Seharusnya %3$s</string> <string name="indexer_section">Relay Indeks</string> <string name="indexer_section_explainer">Relay yang berspesialisasi dalam menyimpan metadata dan daftar relay semua orang, seperti purplepag.es. Amethyst akan menggunakan relay ini untuk menemukan pengguna yang tidak ada dalam daftar Anda.</string> <string name="interest_set_creation_screen_title">Kumpulan Minat Baru</string> @@ -2618,11 +2598,8 @@ Seharusnya %3$s</string> <string name="proxy_section">Relay Proxy</string> <string name="proxy_section_explainer">Relay agregator yang harus digunakan aplikasi untuk mengunduh feed Anda, seperti filter.nostr.wine. Ini menggantikan model outbox dan akan membuat aplikasi hanya terhubung ke relay dalam daftar Anda.</string> <string name="public_chat">Obrolan Publik</string> - <string name="public_chat_explainer">Obrolan publik terlihat oleh semua orang di Nostr dan siapa pun - dapat berpartisipasi. Sangat cocok untuk komunitas terbuka seputar topik tertentu. - Moderasi dapat dilakukan dengan menghapus postingan di relay-nya</string> - <string name="public_chat_relays_explainer">Masukkan antara 1–3 relay yang menjadi host grup ini. - Klien Nostr menggunakan pengaturan ini untuk mengetahui tempat mengunduh dan mengirim pesan Anda.</string> + <string name="public_chat_explainer">Obrolan publik terlihat oleh semua orang di Nostr dan siapa pun dapat berpartisipasi. Sangat cocok untuk komunitas terbuka seputar topik tertentu. Moderasi dapat dilakukan dengan menghapus postingan di relay-nya</string> + <string name="public_chat_relays_explainer">Masukkan antara 1–3 relay yang menjadi host grup ini. Klien Nostr menggunakan pengaturan ini untuk mengetahui tempat mengunduh dan mengirim pesan Anda.</string> <string name="public_chat_relays_title">relay</string> <string name="public_chat_title">Metadata Obrolan Publik</string> <string name="public_chats">Obrolan Publik</string> @@ -2647,10 +2624,7 @@ Seharusnya %3$s</string> <string name="read_only_user">Pengguna hanya-baca</string> <string name="record_a_message_description">Tekan dan tahan untuk merekam pesan</string> <string name="relay_chat">Obrolan relay</string> - <string name="relay_chat_explainer">Obrolan relay adalah grup obrolan yang dikendalikan oleh relay induknya. - Obrolan ini terlihat oleh semua orang di Nostr dan siapa pun dapat berpartisipasi. - Sangat cocok untuk komunitas terbuka seputar topik tertentu. Beberapa grup ini bersifat sementara - sehingga pesan obrolan akan menghilang seiring waktu</string> + <string name="relay_chat_explainer">Obrolan relay adalah grup obrolan yang dikendalikan oleh relay induknya. Obrolan ini terlihat oleh semua orang di Nostr dan siapa pun dapat berpartisipasi. Sangat cocok untuk komunitas terbuka seputar topik tertentu. Beberapa grup ini bersifat sementara sehingga pesan obrolan akan menghilang seiring waktu</string> <string name="relay_chat_title">Obrolan relay</string> <string name="relay_management_tab_events">Acara</string> <string name="relay_management_tab_ips">IP</string> diff --git a/commonsUI/src/commonMain/composeResources/values-in/strings.xml b/commonsUI/src/commonMain/composeResources/values-in/strings.xml index 7635510369..2fe9f6457e 100644 --- a/commonsUI/src/commonMain/composeResources/values-in/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-in/strings.xml @@ -172,7 +172,7 @@ <string name="username">Nama pengguna</string> <string name="about_me">Tentang saya</string> <string name="avatar_url">URL Avatar</string> - <string name="banner_url">URL Spanduk/Banner </string> + <string name="banner_url">URL Spanduk/Banner</string> <string name="website_url">URL Situs Web</string> <string name="pronouns">Kata ganti</string> <string name="stale_relay_hint_label">relay sumber mungkin sudah usang</string> @@ -257,17 +257,8 @@ <string name="mark_all_known_as_read">Tandai semua yg dikenal telah dibaca</string> <string name="mark_all_new_as_read">Tandai pesan baru telah dibaca</string> <string name="mark_all_as_read">Tandai semua telah dibaca</string> - <string name="account_backup_tips2_md"> - ## Tips Cadangan Kunci dan Keamanan - \n\nAkun Anda diamankan dengan kunci rahasia. Kunci ini adalah rangkaian karakter panjang yang diawali dengan **nsec1**. Siapa pun yang memiliki akses ke kunci rahasia ini dapat memposting dan mengubah identitas Anda. - \n\n- **Jangan** memasukkan kunci rahasia Anda ke situs web atau perangkat lunak yang tidak Anda percaya. - \n- Pengembang Amethyst **tidak akan pernah** meminta kunci rahasia Anda. - \n- **Lakukan** pencadangan kunci rahasia Anda secara aman untuk pemulihan akun. Kami merekomendasikan penggunaan pengelola kata sandi. - </string> - <string name="account_backup_tips3_md"> - Untuk keamanan tambahan, Anda dapat mengenkripsi kunci dengan kata sandi. Kunci ini diawali dengan **ncryptsec1** dan tidak dapat digunakan tanpa kata sandi Anda. - \n\nJika Anda kehilangan kata sandi, Anda tidak akan dapat memulihkan kunci Anda. - </string> + <string name="account_backup_tips2_md">## Tips Cadangan Kunci dan Keamanan \n\nAkun Anda diamankan dengan kunci rahasia. Kunci ini adalah rangkaian karakter panjang yang diawali dengan **nsec1**. Siapa pun yang memiliki akses ke kunci rahasia ini dapat memposting dan mengubah identitas Anda. \n\n- **Jangan** memasukkan kunci rahasia Anda ke situs web atau perangkat lunak yang tidak Anda percaya. \n- Pengembang Amethyst **tidak akan pernah** meminta kunci rahasia Anda. \n- **Lakukan** pencadangan kunci rahasia Anda secara aman untuk pemulihan akun. Kami merekomendasikan penggunaan pengelola kata sandi.</string> + <string name="account_backup_tips3_md">Untuk keamanan tambahan, Anda dapat mengenkripsi kunci dengan kata sandi. Kunci ini diawali dengan **ncryptsec1** dan tidak dapat digunakan tanpa kata sandi Anda. \n\nJika Anda kehilangan kata sandi, Anda tidak akan dapat memulihkan kunci Anda.</string> <string name="encrypt_and_copy_my_secret_key">Enkripsi dan salin kunci rahasia saya</string> <string name="badge_award_image_for">Gambar penghargaan lencana untuk %1$s</string> <string name="badge_award_image">"Gambar penghargaan lencana</string> @@ -960,10 +951,7 @@ <string name="push_server_none_explainer">Menonaktifkan Notifikasi Push</string> <string name="push_server_uses_app_explainer">Menggunakan aplikasi %1$s</string> <string name="push_server_install_app">Pengaturan Notifikasi Push</string> - <string name="push_server_install_app_description"> - Untuk menerima notifikasi push, instal aplikasi yang mendukung [Unified Push](https://unifiedpush.org/), seperti [Ntfy](https://ntfy.sh/). - Setelah menginstal, pilih aplikasi yang ingin Anda gunakan di Pengaturan. - </string> + <string name="push_server_install_app_description">Untuk menerima notifikasi push, instal aplikasi yang mendukung [Unified Push](https://unifiedpush.org/), seperti [Ntfy](https://ntfy.sh/). Setelah menginstal, pilih aplikasi yang ingin Anda gunakan di Pengaturan.</string> <string name="thread_title">Utas</string> <string name="send_the_seller_a_message">Kirim pesan ke penjual</string> <string name="hi_there_is_this_still_available">Halo, apakah ini masih tersedia?</string> @@ -2048,15 +2036,7 @@ <string name="could_not_fetch_invoice_from_details">Tidak dapat mengambil invoice dari %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Tidak dapat membuka file terkompresi</string> <string name="couldnt_find_nwc_wallets">Dompet tidak ditemukan</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst tidak dapat menemukan aplikasi dompet apa pun yang mendukung Nostr Wallet Connect (NWC). -\n\nYang dapat Anda lakukan: -\n\t1. Verifikasi Dukungan NWC: Pastikan aplikasi dompet pilihan Anda kompatibel dengan Nostr Wallet Connect. -\n\t2. Buat Koneksi: Buka aplikasi dompet Anda dan buat koneksi NWC baru. -\n\t3. Hubungkan ke Amethyst: Dompet Anda biasanya akan memberikan tiga pilihan: -\n\t\t3.1 Tautan Otomatis: Tombol untuk secara otomatis membuka dan terhubung ke Amethyst. Jika ada, klik tombol tersebut, buka Amethyst dan tekan simpan di layar baru. -\n\t\t3.2 Tautan Manual: URI Koneksi yang dapat Anda salin dan klik ikon tempel di layar ini untuk memasukkannya secara langsung. -\n\t\t3.3 Kode QR: Dompet membuat kode QR yang dapat Anda baca dari tombol kode QR di layar ini. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst tidak dapat menemukan aplikasi dompet apa pun yang mendukung Nostr Wallet Connect (NWC). \n\nYang dapat Anda lakukan: \n\t1. Verifikasi Dukungan NWC: Pastikan aplikasi dompet pilihan Anda kompatibel dengan Nostr Wallet Connect. \n\t2. Buat Koneksi: Buka aplikasi dompet Anda dan buat koneksi NWC baru. \n\t3. Hubungkan ke Amethyst: Dompet Anda biasanya akan memberikan tiga pilihan: \n\t\t3.1 Tautan Otomatis: Tombol untuk secara otomatis membuka dan terhubung ke Amethyst. Jika ada, klik tombol tersebut, buka Amethyst dan tekan simpan di layar baru. \n\t\t3.2 Tautan Manual: URI Koneksi yang dapat Anda salin dan klik ikon tempel di layar ini untuk memasukkannya secara langsung. \n\t\t3.3 Kode QR: Dompet membuat kode QR yang dapat Anda baca dari tombol kode QR di layar ini.</string> <string name="create">Buat</string> <string name="d">hari</string> <string name="danger_zone">Zona Berbahaya</string> @@ -2723,11 +2703,8 @@ <string name="proxy_section">Relay Proxy</string> <string name="proxy_section_explainer">Relay agregator yang harus digunakan aplikasi untuk mengunduh feed Anda, seperti filter.nostr.wine. Ini menggantikan model outbox dan akan membuat aplikasi hanya terhubung ke relay dalam daftar Anda.</string> <string name="public_chat">Percakapan Publik</string> - <string name="public_chat_explainer">Obrolan publik terlihat oleh semua orang di Nostr dan siapa pun - dapat berpartisipasi. Sangat cocok untuk komunitas terbuka seputar topik tertentu. - Moderasi dapat dilakukan dengan menghapus postingan di relay-nya</string> - <string name="public_chat_relays_explainer">Masukkan antara 1–3 relay yang menjadi host grup ini. - Klien Nostr menggunakan pengaturan ini untuk mengetahui tempat mengunduh dan mengirim pesan Anda.</string> + <string name="public_chat_explainer">Obrolan publik terlihat oleh semua orang di Nostr dan siapa pun dapat berpartisipasi. Sangat cocok untuk komunitas terbuka seputar topik tertentu. Moderasi dapat dilakukan dengan menghapus postingan di relay-nya</string> + <string name="public_chat_relays_explainer">Masukkan antara 1–3 relay yang menjadi host grup ini. Klien Nostr menggunakan pengaturan ini untuk mengetahui tempat mengunduh dan mengirim pesan Anda.</string> <string name="public_chat_relays_title">relay</string> <string name="public_chat_title">Metadata Obrolan Publik</string> <string name="public_chats">Obrolan Publik</string> @@ -2752,10 +2729,7 @@ <string name="read_only_user">Pengguna hanya-baca</string> <string name="record_a_message_description">Tekan dan tahan untuk merekam pesan</string> <string name="relay_chat">Obrolan relay</string> - <string name="relay_chat_explainer">Obrolan relay adalah grup obrolan yang dikendalikan oleh relay induknya. - Obrolan ini terlihat oleh semua orang di Nostr dan siapa pun dapat berpartisipasi. - Sangat cocok untuk komunitas terbuka seputar topik tertentu. Beberapa grup ini bersifat sementara - sehingga pesan obrolan akan menghilang seiring waktu</string> + <string name="relay_chat_explainer">Obrolan relay adalah grup obrolan yang dikendalikan oleh relay induknya. Obrolan ini terlihat oleh semua orang di Nostr dan siapa pun dapat berpartisipasi. Sangat cocok untuk komunitas terbuka seputar topik tertentu. Beberapa grup ini bersifat sementara sehingga pesan obrolan akan menghilang seiring waktu</string> <string name="relay_chat_title">Obrolan relay</string> <string name="relay_management_tab_events">Acara</string> <string name="relay_management_tab_ips">IP</string> diff --git a/commonsUI/src/commonMain/composeResources/values-it-rIT/strings.xml b/commonsUI/src/commonMain/composeResources/values-it-rIT/strings.xml index aff7bdaf21..54245dec39 100644 --- a/commonsUI/src/commonMain/composeResources/values-it-rIT/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-it-rIT/strings.xml @@ -266,15 +266,8 @@ <string name="mark_all_known_as_read">Segna tutti i conosciuti come letti</string> <string name="mark_all_new_as_read">Segna tutti i nuovi come letti</string> <string name="mark_all_as_read">Segna tutti come letti</string> - <string name="account_backup_tips2_md"> ## Suggerimenti per il backup e la sicurezza della chiave - \n\nIl tuo account è protetto da una chiave segreta. La chiave è una lunga sequenza di caratteri che inizia con **nsec1**. Chiunque abbia accesso a questa chiave segreta può pubblicare e modificare la tua identità. - \n\n- **Non** inserire la tua chiave segreta in nessun sito web o software di cui non ti fidi. - \n- Gli sviluppatori di Amethyst non chiederanno **mai** la tua chiave segreta. - \n- **Conserva** un backup sicuro della tua chiave segreta per il recupero dell'account. Si consiglia l'uso di un gestore di password. - </string> - <string name="account_backup_tips3_md"> Per maggiore sicurezza, puoi cifrare la tua chiave con una password. Questa chiave inizia con **ncryptsec1** e non può essere utilizzata senza la tua password. - \n\nSe perdi la password, non potrai recuperare la tua chiave. - </string> + <string name="account_backup_tips2_md">## Suggerimenti per il backup e la sicurezza della chiave \n\nIl tuo account è protetto da una chiave segreta. La chiave è una lunga sequenza di caratteri che inizia con **nsec1**. Chiunque abbia accesso a questa chiave segreta può pubblicare e modificare la tua identità. \n\n- **Non** inserire la tua chiave segreta in nessun sito web o software di cui non ti fidi. \n- Gli sviluppatori di Amethyst non chiederanno **mai** la tua chiave segreta. \n- **Conserva** un backup sicuro della tua chiave segreta per il recupero dell'account. Si consiglia l'uso di un gestore di password.</string> + <string name="account_backup_tips3_md">Per maggiore sicurezza, puoi cifrare la tua chiave con una password. Questa chiave inizia con **ncryptsec1** e non può essere utilizzata senza la tua password. \n\nSe perdi la password, non potrai recuperare la tua chiave.</string> <string name="encrypt_and_copy_my_secret_key">Cifra e copia la mia chiave segreta</string> <string name="badge_award_image_for">Immagine premio badge per %1$s</string> <string name="badge_award_image">"Immagine del premio badge</string> @@ -947,9 +940,7 @@ <string name="push_server_none_explainer">Disabilita le notifiche push</string> <string name="push_server_uses_app_explainer">Usa l'app %1$s</string> <string name="push_server_install_app">Configurazione notifiche push</string> - <string name="push_server_install_app_description"> Per ricevere notifiche push, installa qualsiasi app che supporta [Unified Push](https://unifiedpush.org/), come [Ntfy](https://ntfy.sh/). - Dopo l'installazione, seleziona l'app che desideri usare nelle Impostazioni. - </string> + <string name="push_server_install_app_description">Per ricevere notifiche push, installa qualsiasi app che supporta [Unified Push](https://unifiedpush.org/), come [Ntfy](https://ntfy.sh/). Dopo l'installazione, seleziona l'app che desideri usare nelle Impostazioni.</string> <string name="send_the_seller_a_message">Invia un messaggio al venditore</string> <string name="hi_there_is_this_still_available">Ciao, è ancora disponibile?</string> <string name="classifieds_title">Titolo</string> @@ -1986,15 +1977,7 @@ <string name="could_not_fetch_invoice_from_details">Impossibile recuperare la fattura da %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Impossibile aprire il file compresso</string> <string name="couldnt_find_nwc_wallets">Wallet non trovato</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst non ha trovato app wallet compatibili con Nostr Wallet Connect (NWC). -\n\nCosa puoi fare: -\n\t1. Verifica il supporto NWC: controlla che la tua app wallet preferita sia compatibile con Nostr Wallet Connect. -\n\t2. Crea connessione: vai nella tua app wallet e crea una nuova connessione NWC. -\n\t3. Collega ad Amethyst: la tua app wallet ti offrirà solitamente tre opzioni: -\n\t\t3.1 Collegamento automatico: un pulsante per aprire e connettersi automaticamente ad Amethyst. In tal caso, clicca quel pulsante, apri Amethyst e premi salva nella nuova schermata. -\n\t\t3.2 Collegamento manuale: un URI di connessione che puoi copiare e incollare tramite l'icona di incolla in questa schermata. -\n\t\t3.3 Codice QR: il wallet crea un codice QR che puoi leggere tramite il pulsante QR in questa schermata. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst non ha trovato app wallet compatibili con Nostr Wallet Connect (NWC). \n\nCosa puoi fare: \n\t1. Verifica il supporto NWC: controlla che la tua app wallet preferita sia compatibile con Nostr Wallet Connect. \n\t2. Crea connessione: vai nella tua app wallet e crea una nuova connessione NWC. \n\t3. Collega ad Amethyst: la tua app wallet ti offrirà solitamente tre opzioni: \n\t\t3.1 Collegamento automatico: un pulsante per aprire e connettersi automaticamente ad Amethyst. In tal caso, clicca quel pulsante, apri Amethyst e premi salva nella nuova schermata. \n\t\t3.2 Collegamento manuale: un URI di connessione che puoi copiare e incollare tramite l'icona di incolla in questa schermata. \n\t\t3.3 Codice QR: il wallet crea un codice QR che puoi leggere tramite il pulsante QR in questa schermata.</string> <string name="create">Crea</string> <string name="d">g</string> <string name="danger_zone">Zona pericolosa</string> @@ -2645,11 +2628,8 @@ <string name="proxy_section">Relay proxy</string> <string name="proxy_section_explainer">Relay aggregatori che l'app deve usare per scaricare i tuoi feed, come filter.nostr.wine. Questo sostituisce il modello outbox e farà connettere l'app solo ai relay nelle tue liste.</string> <string name="public_chat">Chat Pubblica</string> - <string name="public_chat_explainer">Le chat pubbliche sono visibili a tutti su Nostr e chiunque - può parteciparvi. Sono ideali per comunità aperte su argomenti specifici. - La moderazione può essere controllata eliminando i post sui relay associati</string> - <string name="public_chat_relays_explainer">Inserisci tra 1 e 3 relay che ospitano questo gruppo. - I client Nostr usano questa impostazione per sapere da dove scaricare i messaggi e dove inviare i tuoi messaggi.</string> + <string name="public_chat_explainer">Le chat pubbliche sono visibili a tutti su Nostr e chiunque può parteciparvi. Sono ideali per comunità aperte su argomenti specifici. La moderazione può essere controllata eliminando i post sui relay associati</string> + <string name="public_chat_relays_explainer">Inserisci tra 1 e 3 relay che ospitano questo gruppo. I client Nostr usano questa impostazione per sapere da dove scaricare i messaggi e dove inviare i tuoi messaggi.</string> <string name="public_chat_relays_title">Relay</string> <string name="public_chat_title">Metadati chat pubblica</string> <string name="public_chats">Chat pubbliche</string> @@ -2674,10 +2654,7 @@ <string name="read_only_user">Utente in sola lettura</string> <string name="record_a_message_description">Tieni premuto per registrare un messaggio</string> <string name="relay_chat">Chat relay</string> - <string name="relay_chat_explainer">Le chat relay sono gruppi di chat controllati dal loro relay di riferimento. - Sono visibili a tutti su Nostr e chiunque può parteciparvi. - Sono ideali per comunità aperte su argomenti specifici. Alcuni di questi gruppi sono effimeri - e quindi i messaggi della chat scompaiono nel tempo</string> + <string name="relay_chat_explainer">Le chat relay sono gruppi di chat controllati dal loro relay di riferimento. Sono visibili a tutti su Nostr e chiunque può parteciparvi. Sono ideali per comunità aperte su argomenti specifici. Alcuni di questi gruppi sono effimeri e quindi i messaggi della chat scompaiono nel tempo</string> <string name="relay_chat_title">Chat relay</string> <string name="relay_management_tab_events">Eventi</string> <string name="relay_management_tab_ips">IP</string> diff --git a/commonsUI/src/commonMain/composeResources/values-ja-rJP/strings.xml b/commonsUI/src/commonMain/composeResources/values-ja-rJP/strings.xml index 04d9abd43e..089c9f9857 100644 --- a/commonsUI/src/commonMain/composeResources/values-ja-rJP/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ja-rJP/strings.xml @@ -69,7 +69,7 @@ <string name="scan_qr">QRコードをスキャン</string> <string name="show_anyway">それでも表示する</string> <string name="post_was_hidden">このポストは、非表示にしているユーザーまたはワードが含まれているため隠されています</string> - <string name="post_was_flagged_as_inappropriate_by">このユーザによって投稿が不適切なものとしてマークされています: </string> + <string name="post_was_flagged_as_inappropriate_by">このユーザによって投稿が不適切なものとしてマークされています:</string> <string name="post_not_found">投稿が見つかりません</string> <string name="channel_image">チャンネル画像</string> <string name="referenced_event_not_found">参照先のイベントが見つかりません</string> @@ -271,15 +271,8 @@ <string name="mark_all_known_as_read">すべての「知り合い」を既読にする</string> <string name="mark_all_new_as_read">すべての「新規リクエスト」を既読にする</string> <string name="mark_all_as_read">すべて既読にする</string> - <string name="account_backup_tips2_md"> ## 鍵のバックアップとセキュリティのヒント - \n\nアカウントは秘密鍵によって保護されています。この鍵は **nsec1** で始まる長い文字列です。この秘密鍵にアクセスできる人は誰でも、あなたの名前でポストしたり、プロフィールを変更したりできます。 - \n\n- 信頼していないウェブサイトやアプリに秘密鍵を **入力しないでください**。 - \n- Amethyst の開発者が秘密鍵を **求めることは絶対にありません**。 - \n- アカウントの復元のために、秘密鍵の安全なバックアップを **必ず** 保管してください。パスワードマネージャーの使用をお勧めします。 - </string> - <string name="account_backup_tips3_md"> さらにセキュリティを高めるために、鍵をパスワードで暗号化することができます。この鍵は **ncryptsec1** で始まり、パスワードなしでは使用できません。 - \n\nパスワードを忘れると、鍵を復元できなくなります。 - </string> + <string name="account_backup_tips2_md">## 鍵のバックアップとセキュリティのヒント \n\nアカウントは秘密鍵によって保護されています。この鍵は **nsec1** で始まる長い文字列です。この秘密鍵にアクセスできる人は誰でも、あなたの名前でポストしたり、プロフィールを変更したりできます。 \n\n- 信頼していないウェブサイトやアプリに秘密鍵を **入力しないでください**。 \n- Amethyst の開発者が秘密鍵を **求めることは絶対にありません**。 \n- アカウントの復元のために、秘密鍵の安全なバックアップを **必ず** 保管してください。パスワードマネージャーの使用をお勧めします。</string> + <string name="account_backup_tips3_md">さらにセキュリティを高めるために、鍵をパスワードで暗号化することができます。この鍵は **ncryptsec1** で始まり、パスワードなしでは使用できません。 \n\nパスワードを忘れると、鍵を復元できなくなります。</string> <string name="encrypt_and_copy_my_secret_key">秘密鍵を暗号化してコピー</string> <string name="badge_award_image_for">%1$sのバッジ授与画像</string> <string name="badge_award_image">バッジ授与の画像</string> @@ -965,9 +958,7 @@ <string name="push_server_none_explainer">プッシュ通知を無効にします</string> <string name="push_server_uses_app_explainer">アプリ%1$sを使用</string> <string name="push_server_install_app">プッシュ通知の設定</string> - <string name="push_server_install_app_description"> プッシュ通知を受け取るには、[Ntfy](https://ntfy.sh/)など[Unified Push](https://unifiedpush.org/)に対応したアプリをインストールしてください。 - インストール後、設定で使用するアプリを選択してください。 - </string> + <string name="push_server_install_app_description">プッシュ通知を受け取るには、[Ntfy](https://ntfy.sh/)など[Unified Push](https://unifiedpush.org/)に対応したアプリをインストールしてください。 インストール後、設定で使用するアプリを選択してください。</string> <string name="thread_title">スレッド</string> <string name="send_the_seller_a_message">出品者にメッセージを送る</string> <string name="hi_there_is_this_still_available">こんにちは、こちらはまだ入手可能ですか?</string> @@ -2016,15 +2007,7 @@ <string name="could_not_fetch_invoice_from_details">%1$s から請求書を取得できませんでした: %2$s</string> <string name="could_not_open_the_compressed_file">圧縮ファイルを開けませんでした</string> <string name="couldnt_find_nwc_wallets">ウォレットが見つかりません</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst は Nostr Wallet Connect(NWC)に対応したウォレットアプリを見つけられませんでした。 -\n\n対処方法: -\n\t1. NWC サポートを確認: お使いのウォレットアプリが Nostr Wallet Connect に対応しているか再確認してください。 -\n\t2. 接続を作成: ウォレットアプリで新しい NWC 接続を作成してください。 -\n\t3. Amethyst にリンク: ウォレットは通常、以下の3つの方法を提供します: -\n\t\t3.1 自動リンク: 自動的に Amethyst を開いて接続するボタン。その場合、ボタンをクリックして Amethyst を開き、新しい画面で保存を押してください。 -\n\t\t3.2 手動リンク: この画面の貼り付けアイコンをクリックして直接入力できる接続 URI。 -\n\t\t3.3 QR コード: ウォレットが作成する QR コード。この画面の QR コードボタンから読み取れます。 - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst は Nostr Wallet Connect(NWC)に対応したウォレットアプリを見つけられませんでした。 \n\n対処方法: \n\t1. NWC サポートを確認: お使いのウォレットアプリが Nostr Wallet Connect に対応しているか再確認してください。 \n\t2. 接続を作成: ウォレットアプリで新しい NWC 接続を作成してください。 \n\t3. Amethyst にリンク: ウォレットは通常、以下の3つの方法を提供します: \n\t\t3.1 自動リンク: 自動的に Amethyst を開いて接続するボタン。その場合、ボタンをクリックして Amethyst を開き、新しい画面で保存を押してください。 \n\t\t3.2 手動リンク: この画面の貼り付けアイコンをクリックして直接入力できる接続 URI。 \n\t\t3.3 QR コード: ウォレットが作成する QR コード。この画面の QR コードボタンから読み取れます。</string> <string name="create">作成</string> <string name="d">日</string> <string name="danger_zone">危険ゾーン</string> @@ -2667,11 +2650,8 @@ <string name="proxy_section">Proxy Relay</string> <string name="proxy_section_explainer">filter.nostr.wine のように、アプリがフィードをダウンロードするために使用するアグリゲーター relay です。これにより outbox モデルが置き換えられ、アプリはリスト内の relay のみに接続するようになります。</string> <string name="public_chat">公開チャット</string> - <string name="public_chat_explainer">パブリックチャットは Nostr 上の全員に公開されており、誰でも - 参加できます。特定のトピックを中心としたオープンなコミュニティに最適です。 - モデレーションは、その relay 上のポストを削除することで管理できます</string> - <string name="public_chat_relays_explainer">このグループをホストする relay を 1〜3 件入力してください。 - Nostr クライアントはこの設定を使用して、メッセージのダウンロード先と送信先を決定します。</string> + <string name="public_chat_explainer">パブリックチャットは Nostr 上の全員に公開されており、誰でも 参加できます。特定のトピックを中心としたオープンなコミュニティに最適です。 モデレーションは、その relay 上のポストを削除することで管理できます</string> + <string name="public_chat_relays_explainer">このグループをホストする relay を 1〜3 件入力してください。 Nostr クライアントはこの設定を使用して、メッセージのダウンロード先と送信先を決定します。</string> <string name="public_chat_relays_title">Relay</string> <string name="public_chat_title">パブリックチャットのメタデータ</string> <string name="public_chats">パブリックチャット</string> @@ -2696,10 +2676,7 @@ <string name="read_only_user">読み取り専用ユーザー</string> <string name="record_a_message_description">長押ししてメッセージを録音</string> <string name="relay_chat">relay チャット</string> - <string name="relay_chat_explainer">relay チャットは、ホスト relay によって管理されるチャットグループです。 - Nostr 上の全員に公開されており、誰でも参加できます。 - 特定のトピックを中心としたオープンなコミュニティに最適です。一部のグループは一時的なもので、 - チャットメッセージは時間とともに消えます</string> + <string name="relay_chat_explainer">relay チャットは、ホスト relay によって管理されるチャットグループです。 Nostr 上の全員に公開されており、誰でも参加できます。 特定のトピックを中心としたオープンなコミュニティに最適です。一部のグループは一時的なもので、 チャットメッセージは時間とともに消えます</string> <string name="relay_chat_title">relay チャット</string> <string name="relay_management_tab_events">イベント</string> <string name="relay_management_tab_ips">IP</string> diff --git a/commonsUI/src/commonMain/composeResources/values-ja/strings.xml b/commonsUI/src/commonMain/composeResources/values-ja/strings.xml index a9b6f5afcf..8e2365c27e 100644 --- a/commonsUI/src/commonMain/composeResources/values-ja/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ja/strings.xml @@ -49,7 +49,7 @@ <string name="scan_qr">QRコードをスキャン</string> <string name="show_anyway">それでも表示する</string> <string name="post_was_hidden">このポストは、非表示にしているユーザーまたはワードが含まれているため隠されています</string> - <string name="post_was_flagged_as_inappropriate_by">このユーザによって投稿が不適切なものとしてマークされています: </string> + <string name="post_was_flagged_as_inappropriate_by">このユーザによって投稿が不適切なものとしてマークされています:</string> <string name="post_not_found">投稿が見つかりません</string> <string name="post_not_found_short">👀</string> <string name="channel_image">チャンネル画像</string> @@ -257,17 +257,8 @@ <string name="mark_all_known_as_read">すべての「知り合い」を既読にする</string> <string name="mark_all_new_as_read">すべての「新規リクエスト」を既読にする</string> <string name="mark_all_as_read">すべて既読にする</string> - <string name="account_backup_tips2_md"> - ## 鍵のバックアップとセキュリティのヒント - \n\nアカウントは秘密鍵によって保護されています。この鍵は **nsec1** で始まる長い文字列です。この秘密鍵にアクセスできる人は誰でも、あなたの名前でポストしたり、プロフィールを変更したりできます。 - \n\n- 信頼していないウェブサイトやアプリに秘密鍵を **入力しないでください**。 - \n- Amethyst の開発者が秘密鍵を **求めることは絶対にありません**。 - \n- アカウントの復元のために、秘密鍵の安全なバックアップを **必ず** 保管してください。パスワードマネージャーの使用をお勧めします。 - </string> - <string name="account_backup_tips3_md"> - さらにセキュリティを高めるために、鍵をパスワードで暗号化することができます。この鍵は **ncryptsec1** で始まり、パスワードなしでは使用できません。 - \n\nパスワードを忘れると、鍵を復元できなくなります。 - </string> + <string name="account_backup_tips2_md">## 鍵のバックアップとセキュリティのヒント \n\nアカウントは秘密鍵によって保護されています。この鍵は **nsec1** で始まる長い文字列です。この秘密鍵にアクセスできる人は誰でも、あなたの名前でポストしたり、プロフィールを変更したりできます。 \n\n- 信頼していないウェブサイトやアプリに秘密鍵を **入力しないでください**。 \n- Amethyst の開発者が秘密鍵を **求めることは絶対にありません**。 \n- アカウントの復元のために、秘密鍵の安全なバックアップを **必ず** 保管してください。パスワードマネージャーの使用をお勧めします。</string> + <string name="account_backup_tips3_md">さらにセキュリティを高めるために、鍵をパスワードで暗号化することができます。この鍵は **ncryptsec1** で始まり、パスワードなしでは使用できません。 \n\nパスワードを忘れると、鍵を復元できなくなります。</string> <string name="encrypt_and_copy_my_secret_key">秘密鍵を暗号化してコピー</string> <string name="badge_award_image_for">%1$sのバッジ授与画像</string> <string name="badge_award_image">バッジ授与の画像</string> @@ -960,10 +951,7 @@ <string name="push_server_none_explainer">プッシュ通知を無効にします</string> <string name="push_server_uses_app_explainer">アプリ%1$sを使用</string> <string name="push_server_install_app">プッシュ通知の設定</string> - <string name="push_server_install_app_description"> - プッシュ通知を受け取るには、[Ntfy](https://ntfy.sh/)など[Unified Push](https://unifiedpush.org/)に対応したアプリをインストールしてください。 - インストール後、設定で使用するアプリを選択してください。 - </string> + <string name="push_server_install_app_description">プッシュ通知を受け取るには、[Ntfy](https://ntfy.sh/)など[Unified Push](https://unifiedpush.org/)に対応したアプリをインストールしてください。 インストール後、設定で使用するアプリを選択してください。</string> <string name="thread_title">スレッド</string> <string name="send_the_seller_a_message">出品者にメッセージを送る</string> <string name="hi_there_is_this_still_available">こんにちは、こちらはまだ入手可能ですか?</string> @@ -2048,15 +2036,7 @@ <string name="could_not_fetch_invoice_from_details">%1$s から請求書を取得できませんでした: %2$s</string> <string name="could_not_open_the_compressed_file">圧縮ファイルを開けませんでした</string> <string name="couldnt_find_nwc_wallets">ウォレットが見つかりません</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst は Nostr Wallet Connect(NWC)に対応したウォレットアプリを見つけられませんでした。 -\n\n対処方法: -\n\t1. NWC サポートを確認: お使いのウォレットアプリが Nostr Wallet Connect に対応しているか再確認してください。 -\n\t2. 接続を作成: ウォレットアプリで新しい NWC 接続を作成してください。 -\n\t3. Amethyst にリンク: ウォレットは通常、以下の3つの方法を提供します: -\n\t\t3.1 自動リンク: 自動的に Amethyst を開いて接続するボタン。その場合、ボタンをクリックして Amethyst を開き、新しい画面で保存を押してください。 -\n\t\t3.2 手動リンク: この画面の貼り付けアイコンをクリックして直接入力できる接続 URI。 -\n\t\t3.3 QR コード: ウォレットが作成する QR コード。この画面の QR コードボタンから読み取れます。 - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst は Nostr Wallet Connect(NWC)に対応したウォレットアプリを見つけられませんでした。 \n\n対処方法: \n\t1. NWC サポートを確認: お使いのウォレットアプリが Nostr Wallet Connect に対応しているか再確認してください。 \n\t2. 接続を作成: ウォレットアプリで新しい NWC 接続を作成してください。 \n\t3. Amethyst にリンク: ウォレットは通常、以下の3つの方法を提供します: \n\t\t3.1 自動リンク: 自動的に Amethyst を開いて接続するボタン。その場合、ボタンをクリックして Amethyst を開き、新しい画面で保存を押してください。 \n\t\t3.2 手動リンク: この画面の貼り付けアイコンをクリックして直接入力できる接続 URI。 \n\t\t3.3 QR コード: ウォレットが作成する QR コード。この画面の QR コードボタンから読み取れます。</string> <string name="create">作成</string> <string name="d">日</string> <string name="danger_zone">危険ゾーン</string> @@ -2723,11 +2703,8 @@ <string name="proxy_section">Proxy Relay</string> <string name="proxy_section_explainer">filter.nostr.wine のように、アプリがフィードをダウンロードするために使用するアグリゲーター relay です。これにより outbox モデルが置き換えられ、アプリはリスト内の relay のみに接続するようになります。</string> <string name="public_chat">公開チャット</string> - <string name="public_chat_explainer">パブリックチャットは Nostr 上の全員に公開されており、誰でも - 参加できます。特定のトピックを中心としたオープンなコミュニティに最適です。 - モデレーションは、その relay 上のポストを削除することで管理できます</string> - <string name="public_chat_relays_explainer">このグループをホストする relay を 1〜3 件入力してください。 - Nostr クライアントはこの設定を使用して、メッセージのダウンロード先と送信先を決定します。</string> + <string name="public_chat_explainer">パブリックチャットは Nostr 上の全員に公開されており、誰でも 参加できます。特定のトピックを中心としたオープンなコミュニティに最適です。 モデレーションは、その relay 上のポストを削除することで管理できます</string> + <string name="public_chat_relays_explainer">このグループをホストする relay を 1〜3 件入力してください。 Nostr クライアントはこの設定を使用して、メッセージのダウンロード先と送信先を決定します。</string> <string name="public_chat_relays_title">Relay</string> <string name="public_chat_title">パブリックチャットのメタデータ</string> <string name="public_chats">パブリックチャット</string> @@ -2752,10 +2729,7 @@ <string name="read_only_user">読み取り専用ユーザー</string> <string name="record_a_message_description">長押ししてメッセージを録音</string> <string name="relay_chat">relay チャット</string> - <string name="relay_chat_explainer">relay チャットは、ホスト relay によって管理されるチャットグループです。 - Nostr 上の全員に公開されており、誰でも参加できます。 - 特定のトピックを中心としたオープンなコミュニティに最適です。一部のグループは一時的なもので、 - チャットメッセージは時間とともに消えます</string> + <string name="relay_chat_explainer">relay チャットは、ホスト relay によって管理されるチャットグループです。 Nostr 上の全員に公開されており、誰でも参加できます。 特定のトピックを中心としたオープンなコミュニティに最適です。一部のグループは一時的なもので、 チャットメッセージは時間とともに消えます</string> <string name="relay_chat_title">relay チャット</string> <string name="relay_management_tab_events">イベント</string> <string name="relay_management_tab_ips">IP</string> diff --git a/commonsUI/src/commonMain/composeResources/values-ko-rKR/strings.xml b/commonsUI/src/commonMain/composeResources/values-ko-rKR/strings.xml index 5ef5365607..4e876d3cf0 100644 --- a/commonsUI/src/commonMain/composeResources/values-ko-rKR/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ko-rKR/strings.xml @@ -266,15 +266,8 @@ <string name="mark_all_known_as_read">알려진 항목 모두 읽음 표시</string> <string name="mark_all_new_as_read">새 항목 모두 읽음 표시</string> <string name="mark_all_as_read">모두 읽음 표시</string> - <string name="account_backup_tips2_md"> ## 키 백업 및 보안 팁 - \n\n계정은 비밀 키로 보호됩니다. 키는 **nsec1**로 시작하는 긴 문자열입니다. 이 비밀 키에 접근할 수 있는 누구든 게시물 작성 및 신원 변경이 가능합니다. - \n\n- 신뢰하지 않는 웹사이트나 소프트웨어에 비밀 키를 **입력하지** 마세요. - \n- Amethyst 개발자는 **절대** 비밀 키를 요청하지 않습니다. - \n- 계정 복구를 위해 비밀 키를 안전하게 백업해 **두세요**. 비밀번호 관리자 사용을 권장합니다. - </string> - <string name="account_backup_tips3_md"> 추가 보안을 위해 비밀번호로 키를 암호화할 수 있습니다. 이 키는 **ncryptsec1**로 시작하며 비밀번호 없이는 사용할 수 없습니다. - \n\n비밀번호를 분실하면 키를 복구할 수 없습니다. - </string> + <string name="account_backup_tips2_md">## 키 백업 및 보안 팁 \n\n계정은 비밀 키로 보호됩니다. 키는 **nsec1**로 시작하는 긴 문자열입니다. 이 비밀 키에 접근할 수 있는 누구든 게시물 작성 및 신원 변경이 가능합니다. \n\n- 신뢰하지 않는 웹사이트나 소프트웨어에 비밀 키를 **입력하지** 마세요. \n- Amethyst 개발자는 **절대** 비밀 키를 요청하지 않습니다. \n- 계정 복구를 위해 비밀 키를 안전하게 백업해 **두세요**. 비밀번호 관리자 사용을 권장합니다.</string> + <string name="account_backup_tips3_md">추가 보안을 위해 비밀번호로 키를 암호화할 수 있습니다. 이 키는 **ncryptsec1**로 시작하며 비밀번호 없이는 사용할 수 없습니다. \n\n비밀번호를 분실하면 키를 복구할 수 없습니다.</string> <string name="encrypt_and_copy_my_secret_key">내 비밀 키 암호화 및 복사</string> <string name="badge_award_image_for">%1$s의 배지 수상 이미지</string> <string name="badge_award_image">"배지 수상 이미지</string> @@ -958,9 +951,7 @@ <string name="push_server_none_explainer">푸시 알림 비활성화</string> <string name="push_server_uses_app_explainer">앱 %1$s 사용</string> <string name="push_server_install_app">푸시 알림 설정</string> - <string name="push_server_install_app_description"> 푸시 알림을 받으려면 [Ntfy](https://ntfy.sh/)와 같은 [Unified Push](https://unifiedpush.org/)를 지원하는 앱을 설치하세요. - 설치 후 설정에서 사용할 앱을 선택하세요. - </string> + <string name="push_server_install_app_description">푸시 알림을 받으려면 [Ntfy](https://ntfy.sh/)와 같은 [Unified Push](https://unifiedpush.org/)를 지원하는 앱을 설치하세요. 설치 후 설정에서 사용할 앱을 선택하세요.</string> <string name="thread_title">스레드</string> <string name="send_the_seller_a_message">판매자에게 메시지 보내기</string> <string name="hi_there_is_this_still_available">안녕하세요, 아직 구매 가능한가요?</string> @@ -2005,15 +1996,7 @@ <string name="could_not_fetch_invoice_from_details">%1$s에서 인보이스를 가져올 수 없습니다: %2$s</string> <string name="could_not_open_the_compressed_file">압축 파일을 열 수 없습니다</string> <string name="couldnt_find_nwc_wallets">지갑을 찾을 수 없음</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst에서 Nostr Wallet Connect (NWC)를 지원하는 지갑 앱을 찾을 수 없습니다. -\n\n할 수 있는 작업: -\n\t1. NWC 지원 확인: 사용하려는 지갑 앱이 Nostr Wallet Connect와 호환되는지 다시 확인하세요. -\n\t2. 연결 생성: 지갑 앱에서 새 NWC 연결을 생성하세요. -\n\t3. Amethyst에 연결: 지갑에서 보통 세 가지 옵션을 제공합니다: -\n\t\t3.1 자동 연결: Amethyst를 자동으로 열고 연결하는 버튼. 해당 버튼을 클릭하고 Amethyst를 열어 새 화면에서 저장을 누르세요. -\n\t\t3.2 수동 연결: 연결 URI를 복사하고 이 화면의 붙여넣기 아이콘을 클릭하여 직접 입력하세요. -\n\t\t3.3 QR 코드: 지갑이 QR 코드를 생성하면 이 화면의 QR 코드 버튼으로 읽을 수 있습니다. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst에서 Nostr Wallet Connect (NWC)를 지원하는 지갑 앱을 찾을 수 없습니다. \n\n할 수 있는 작업: \n\t1. NWC 지원 확인: 사용하려는 지갑 앱이 Nostr Wallet Connect와 호환되는지 다시 확인하세요. \n\t2. 연결 생성: 지갑 앱에서 새 NWC 연결을 생성하세요. \n\t3. Amethyst에 연결: 지갑에서 보통 세 가지 옵션을 제공합니다: \n\t\t3.1 자동 연결: Amethyst를 자동으로 열고 연결하는 버튼. 해당 버튼을 클릭하고 Amethyst를 열어 새 화면에서 저장을 누르세요. \n\t\t3.2 수동 연결: 연결 URI를 복사하고 이 화면의 붙여넣기 아이콘을 클릭하여 직접 입력하세요. \n\t\t3.3 QR 코드: 지갑이 QR 코드를 생성하면 이 화면의 QR 코드 버튼으로 읽을 수 있습니다.</string> <string name="create">만들기</string> <string name="d">일</string> <string name="danger_zone">위험 구역</string> @@ -2653,11 +2636,8 @@ <string name="proxy_section">프록시 Relay</string> <string name="proxy_section_explainer">filter.nostr.wine 같이 피드를 다운로드하는 데 앱이 사용해야 하는 집계 relay입니다. 이 설정은 아웃박스 모델을 대체하며 앱이 목록에 있는 relay에만 연결하도록 합니다.</string> <string name="public_chat">공개 채팅</string> - <string name="public_chat_explainer">공개 채팅은 Nostr의 모든 사람에게 공개되며 - 누구나 참여할 수 있습니다. 특정 주제에 관한 열린 커뮤니티에 적합합니다. - relay에서 게시물을 삭제하여 관리할 수 있습니다</string> - <string name="public_chat_relays_explainer">이 그룹을 호스팅하는 relay를 1~3개 입력하세요. - Nostr 클라이언트는 이 설정을 통해 메시지를 다운로드하고 전송할 위치를 파악합니다.</string> + <string name="public_chat_explainer">공개 채팅은 Nostr의 모든 사람에게 공개되며 누구나 참여할 수 있습니다. 특정 주제에 관한 열린 커뮤니티에 적합합니다. relay에서 게시물을 삭제하여 관리할 수 있습니다</string> + <string name="public_chat_relays_explainer">이 그룹을 호스팅하는 relay를 1~3개 입력하세요. Nostr 클라이언트는 이 설정을 통해 메시지를 다운로드하고 전송할 위치를 파악합니다.</string> <string name="public_chat_relays_title">relay</string> <string name="public_chat_title">공개 채팅 메타데이터</string> <string name="public_chats">공개 채팅</string> @@ -2682,10 +2662,7 @@ <string name="read_only_user">읽기 전용 사용자</string> <string name="record_a_message_description">길게 눌러 메시지를 녹음하세요</string> <string name="relay_chat">relay 채팅</string> - <string name="relay_chat_explainer">relay 채팅은 홈 relay가 관리하는 채팅 그룹입니다. - Nostr의 모든 사람에게 공개되며 누구나 참여할 수 있습니다. - 특정 주제에 관한 열린 커뮤니티에 적합합니다. 일부 그룹은 임시적이라 - 채팅 메시지가 시간이 지나면 사라집니다</string> + <string name="relay_chat_explainer">relay 채팅은 홈 relay가 관리하는 채팅 그룹입니다. Nostr의 모든 사람에게 공개되며 누구나 참여할 수 있습니다. 특정 주제에 관한 열린 커뮤니티에 적합합니다. 일부 그룹은 임시적이라 채팅 메시지가 시간이 지나면 사라집니다</string> <string name="relay_chat_title">relay 채팅</string> <string name="relay_management_tab_events">이벤트</string> <string name="relay_management_tab_ips">IP</string> diff --git a/commonsUI/src/commonMain/composeResources/values-lv-rLV/strings.xml b/commonsUI/src/commonMain/composeResources/values-lv-rLV/strings.xml index 0ce2714a9a..2cb28ecf8f 100644 --- a/commonsUI/src/commonMain/composeResources/values-lv-rLV/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-lv-rLV/strings.xml @@ -277,15 +277,8 @@ <string name="mark_all_known_as_read">Atzīmēt visu zināmo kā lasītu</string> <string name="mark_all_new_as_read">Atzīmēt visu jauno kā lasītu</string> <string name="mark_all_as_read">Atzīmēt visu kā lasītu</string> - <string name="account_backup_tips2_md"> ## Atslēgas dublēšanas un drošības padomi - \n\nJūsu konts ir aizsargāts ar slepeno atslēgu. Atslēga ir gara rakstzīmju secība, kas sākas ar **nsec1**. Ikviens, kam ir piekļuve šai slepenai atslēgai, var publicēt un mainīt jūsu identitāti. - \n\n- **Neievietojiet** savu slepeno atslēgu nevienā tīmekļa vietnē vai programmatūrā, kurai neuzticaties. - \n- Amethyst izstrādātāji **nekad** neprasīs jūsu slepeno atslēgu. - \n- **Glabājiet** drošu slepenas atslēgas dublējumu konta atjaunošanai. Mēs iesakām izmantot paroļu pārvaldnieku. - </string> - <string name="account_backup_tips3_md"> Papildu drošībai variet šifrēt atslēgu ar paroli. Šī atslēga sāksies ar **ncryptsec1** virkni un to nevarēs izmantot bez šīs paroles. - \n\nJa jūs aizmirsīsiet paroli, vairs nevarēsiet atgūt savu atslēgu. - </string> + <string name="account_backup_tips2_md">## Atslēgas dublēšanas un drošības padomi \n\nJūsu konts ir aizsargāts ar slepeno atslēgu. Atslēga ir gara rakstzīmju secība, kas sākas ar **nsec1**. Ikviens, kam ir piekļuve šai slepenai atslēgai, var publicēt un mainīt jūsu identitāti. \n\n- **Neievietojiet** savu slepeno atslēgu nevienā tīmekļa vietnē vai programmatūrā, kurai neuzticaties. \n- Amethyst izstrādātāji **nekad** neprasīs jūsu slepeno atslēgu. \n- **Glabājiet** drošu slepenas atslēgas dublējumu konta atjaunošanai. Mēs iesakām izmantot paroļu pārvaldnieku.</string> + <string name="account_backup_tips3_md">Papildu drošībai variet šifrēt atslēgu ar paroli. Šī atslēga sāksies ar **ncryptsec1** virkni un to nevarēs izmantot bez šīs paroles. \n\nJa jūs aizmirsīsiet paroli, vairs nevarēsiet atgūt savu atslēgu.</string> <string name="backup_keys_nudge_body">Jūsu slepenā atslēga ir vienīgais veids, kā piekļūt šim kontam. Ja to pazaudēsiet, tad nekad nevarēsiet atgūt piekļuvi kontam. Saglabājiet atslēgu drošā vietā tagad.</string> <string name="backup_keys_nudge_already_saved">Esiet tās saglabājuši</string> <string name="encrypt_and_copy_my_secret_key">Šifrēt un kopēt manu slepeno atslēgu</string> @@ -970,9 +963,7 @@ <string name="push_server_none_explainer">Atspējo push paziņojumus</string> <string name="push_server_uses_app_explainer">Izmanto lietotni %1$s</string> <string name="push_server_install_app">Push paziņojumu iestatīšana</string> - <string name="push_server_install_app_description"> Lai saņemtu push paziņojumus, instalējiet jebkuru lietotni, kas atbalsta [Unified Push](https://unifiedpush.org/), piemēram, [Ntfy](https://ntfy.sh/). - Pēc instalēšanas iestatījumos atlasiet lietotni, kuru vēlaties izmantot. - </string> + <string name="push_server_install_app_description">Lai saņemtu push paziņojumus, instalējiet jebkuru lietotni, kas atbalsta [Unified Push](https://unifiedpush.org/), piemēram, [Ntfy](https://ntfy.sh/). Pēc instalēšanas iestatījumos atlasiet lietotni, kuru vēlaties izmantot.</string> <string name="thread_title">Diskusija</string> <string name="send_the_seller_a_message">Nosūtīt pārdevējam ziņojumu</string> <string name="hi_there_is_this_still_available">Sveiki, vai tas joprojām ir pieejams?</string> @@ -2037,15 +2028,7 @@ <string name="could_not_fetch_invoice_from_details">Nevarēja iegūt rēķinu no %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Nevarēja atvērt saspiestu failu</string> <string name="couldnt_find_nwc_wallets">Maks nav atrasts</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst nevarēja atrast nevienu maka lietotni, kas atbalsta Nostr Wallet Connect (NWC). -\n\nKo varat darīt: -\n\t1. Pārbaudiet NWC atbalstu: Pārliecinieties, vai jūsu vēlamā maka lietotne ir saderīga ar Nostr Wallet Connect. -\n\t2. Izveidojiet savienojumu: Atveriet savu maka lietotni un izveidojiet jaunu NWC savienojumu. -\n\t3. Saistiet ar Amethyst: Jūsu maks parasti piedāvās trīs iespējas: -\n\t\t3.1 Automātiska saistīšana: Poga, lai automātiski atvērtu un savienotu ar Amethyst. Ja tā, noklikšķiniet uz šīs pogas, atveriet Amethyst un nospiediet saglabāt jaunajā ekrānā. -\n\t\t3.2 Manuāla saistīšana: Savienojuma URI, ko varat kopēt un noklikšķināt uz ielīmēšanas ikonas šajā ekrānā, lai to ievadītu tieši. -\n\t\t3.3 QR kods: Maks izveido QR kodu, kuru varat nolasīt ar QR koda pogu šajā ekrānā. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst nevarēja atrast nevienu maka lietotni, kas atbalsta Nostr Wallet Connect (NWC). \n\nKo varat darīt: \n\t1. Pārbaudiet NWC atbalstu: Pārliecinieties, vai jūsu vēlamā maka lietotne ir saderīga ar Nostr Wallet Connect. \n\t2. Izveidojiet savienojumu: Atveriet savu maka lietotni un izveidojiet jaunu NWC savienojumu. \n\t3. Saistiet ar Amethyst: Jūsu maks parasti piedāvās trīs iespējas: \n\t\t3.1 Automātiska saistīšana: Poga, lai automātiski atvērtu un savienotu ar Amethyst. Ja tā, noklikšķiniet uz šīs pogas, atveriet Amethyst un nospiediet saglabāt jaunajā ekrānā. \n\t\t3.2 Manuāla saistīšana: Savienojuma URI, ko varat kopēt un noklikšķināt uz ielīmēšanas ikonas šajā ekrānā, lai to ievadītu tieši. \n\t\t3.3 QR kods: Maks izveido QR kodu, kuru varat nolasīt ar QR koda pogu šajā ekrānā.</string> <string name="create">Izveidot</string> <string name="danger_zone">Bīstamā zona</string> <string name="dark">Tumšs</string> @@ -2724,11 +2707,8 @@ <string name="proxy_section">Starpniekservera Relay</string> <string name="proxy_section_explainer">Agregātori relay, ko lietotnei jāizmanto, lai lejupielādētu jūsu plūsmas, piemēram, filter.nostr.wine. Tas aizstāj izejošo ziņojumu modeli un liks lietotnei pieslēgties tikai jūsu sarakstos esošajiem relay.</string> <string name="public_chat">Publiskā tērzēšana</string> - <string name="public_chat_explainer">Publiskās tērzēšanas ir redzamas visiem Nostr lietotājiem un ikviens - var tajās piedalīties. Tās ir lieliski piemērotas atvērtām kopienām par konkrētām tēmām. - Moderēšanu var kontrolēt, dzēšot ierakstus tās relay</string> - <string name="public_chat_relays_explainer">Ievadiet no 1 līdz 3 relay, kas mitina šo grupu. - Nostr klienti izmanto šo iestatījumu, lai zinātu, no kurienes lejupielādēt ziņojumus un kur nosūtīt jūsu ziņojumus.</string> + <string name="public_chat_explainer">Publiskās tērzēšanas ir redzamas visiem Nostr lietotājiem un ikviens var tajās piedalīties. Tās ir lieliski piemērotas atvērtām kopienām par konkrētām tēmām. Moderēšanu var kontrolēt, dzēšot ierakstus tās relay</string> + <string name="public_chat_relays_explainer">Ievadiet no 1 līdz 3 relay, kas mitina šo grupu. Nostr klienti izmanto šo iestatījumu, lai zinātu, no kurienes lejupielādēt ziņojumus un kur nosūtīt jūsu ziņojumus.</string> <string name="public_chat_relays_title">Relay</string> <string name="public_chat_title">Publiskās tērzēšanas metadati</string> <string name="public_chats">Publiskie čati</string> @@ -2753,10 +2733,7 @@ <string name="read_only_user">Tikai lasāms lietotājs</string> <string name="record_a_message_description">Noklikšķiniet un turiet, lai ierakstītu ziņojumu</string> <string name="relay_chat">Relay tērzēšana</string> - <string name="relay_chat_explainer">Relay tērzēšanas ir tērzēšanas grupas, kuras kontrolē to mājas relay. - Tās ir redzamas visiem Nostr lietotājiem un ikviens var tajās piedalīties. - Tās ir lieliski piemērotas atvērtām kopienām par konkrētām tēmām. Dažas no šīm grupām ir īslaicīgas - un tāpēc tērzēšanas ziņojumi laika gaitā pazūd</string> + <string name="relay_chat_explainer">Relay tērzēšanas ir tērzēšanas grupas, kuras kontrolē to mājas relay. Tās ir redzamas visiem Nostr lietotājiem un ikviens var tajās piedalīties. Tās ir lieliski piemērotas atvērtām kopienām par konkrētām tēmām. Dažas no šīm grupām ir īslaicīgas un tāpēc tērzēšanas ziņojumi laika gaitā pazūd</string> <string name="relay_chat_title">Relay tērzēšanas</string> <string name="relay_management_tab_events">Notikumi</string> <string name="relay_management_tab_ips">IP adreses</string> diff --git a/commonsUI/src/commonMain/composeResources/values-nl/strings.xml b/commonsUI/src/commonMain/composeResources/values-nl/strings.xml index 70ca1190a6..5ddca3ba0e 100644 --- a/commonsUI/src/commonMain/composeResources/values-nl/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-nl/strings.xml @@ -211,7 +211,7 @@ <string name="copy_user_id">Kopieer gebruiker ID</string> <string name="unblock_user">Deblokkeer gebruiker</string> <string name="user_is_blocked_hidden">Deze gebruiker is geblokkeerd/gedempt. Hun berichten zijn verborgen.</string> - <string name="npub_hex_username">npub, hex, gebruikersnaam </string> + <string name="npub_hex_username">npub, hex, gebruikersnaam</string> <string name="clear">Wissen</string> <string name="app_logo">App logo</string> <string name="nsec_npub_hex_private_key">nsec.. of npub..</string> @@ -259,15 +259,8 @@ <string name="mark_all_known_as_read">Bekende markeren als gelezen</string> <string name="mark_all_new_as_read">Nieuwe markeren als gelezen</string> <string name="mark_all_as_read">Alles markeren als gelezen</string> - <string name="account_backup_tips2_md"> ## Sleutel back-up en veiligheidstips - \n\nUw account is beveiligd met een privésleutel. De sleutel is een lange, willekeurige reeks die begint met **nsec1**. Iedereen die toegang heeft tot uw privésleutel kan inhoud publiceren met uw identiteit. - \n\n- Voer uw privésleutel **nooit** in een website of software die u niet vertrouwt. - \n- Amethyst ontwikkelaars zullen u **nooit** om uw privésleutel vragen. - \n- **Bewaar** een veilige back-up van uw privésleutel voor accountherstel. Wij raden u aan een wachtwoordmanager te gebruiken. - </string> - <string name="account_backup_tips3_md"> Voor meer veiligheid kan je je sleutel versleutelen met een wachtwoord. Deze sleutel begint met **ncryptsec1** en kan niet worden gebruikt zonder uw wachtwoord. - \n\nAls je je wachtwoord verliest, je kan je sleutel niet herstellen. - </string> + <string name="account_backup_tips2_md">## Sleutel back-up en veiligheidstips \n\nUw account is beveiligd met een privésleutel. De sleutel is een lange, willekeurige reeks die begint met **nsec1**. Iedereen die toegang heeft tot uw privésleutel kan inhoud publiceren met uw identiteit. \n\n- Voer uw privésleutel **nooit** in een website of software die u niet vertrouwt. \n- Amethyst ontwikkelaars zullen u **nooit** om uw privésleutel vragen. \n- **Bewaar** een veilige back-up van uw privésleutel voor accountherstel. Wij raden u aan een wachtwoordmanager te gebruiken.</string> + <string name="account_backup_tips3_md">Voor meer veiligheid kan je je sleutel versleutelen met een wachtwoord. Deze sleutel begint met **ncryptsec1** en kan niet worden gebruikt zonder uw wachtwoord. \n\nAls je je wachtwoord verliest, je kan je sleutel niet herstellen.</string> <string name="encrypt_and_copy_my_secret_key">Versleutel en kopieer mijn geheime sleutel</string> <string name="badge_award_image_for">Badge award afbeelding voor %1$s</string> <string name="badge_award_image">Badge-awardafbeelding</string> @@ -960,9 +953,7 @@ <string name="push_server_none_explainer">Push-notificaties uitschakelen</string> <string name="push_server_uses_app_explainer">Gebruikt app %1$s</string> <string name="push_server_install_app">Instellen Push-notificaties</string> - <string name="push_server_install_app_description"> Om push-notificaties te ontvangen, installeer een app die [Unified Push] ondersteunt](https://unifiedpush.org/), zoals [Ntfy](https://ntfy.sh/). - Na installatie selecteer de app die je wilt gebruiken in de instellingen. - </string> + <string name="push_server_install_app_description">Om push-notificaties te ontvangen, installeer een app die [Unified Push] ondersteunt](https://unifiedpush.org/), zoals [Ntfy](https://ntfy.sh/). Na installatie selecteer de app die je wilt gebruiken in de instellingen.</string> <string name="thread_title">Draad</string> <string name="send_the_seller_a_message">Stuur de verkoper een bericht</string> <string name="hi_there_is_this_still_available">Hallo daar, is dit nog steeds beschikbaar?</string> diff --git a/commonsUI/src/commonMain/composeResources/values-pl-rPL/strings.xml b/commonsUI/src/commonMain/composeResources/values-pl-rPL/strings.xml index 6b3fd3bf9a..570f8fc2b1 100644 --- a/commonsUI/src/commonMain/composeResources/values-pl-rPL/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-pl-rPL/strings.xml @@ -508,8 +508,7 @@ <string name="about_us">Informacja o grupie…</string> <string name="search_and_add_a_user">Szukaj i dodaj użytkownika</string> <string name="add_a_relay">Dodaj Transmiter</string> - <string name="relay_url_not_valid">Nieprawidłowy adres transmitera. Użyj nazwy hosta lub adresu IP w nawiasach (na przykład -[201:d0e:9ba5:8bbc::1]:8080).</string> + <string name="relay_url_not_valid">Nieprawidłowy adres transmitera. Użyj nazwy hosta lub adresu IP w nawiasach (na przykład [201:d0e:9ba5:8bbc::1]:8080).</string> <string name="my_name">Moje imię @tag</string> <string name="display_name">Nazwa użytkownika</string> <string name="my_display_name">Mój nick</string> @@ -657,15 +656,8 @@ <string name="mark_all_known_as_read">Zaznacz wszystkie popularne jako przeczytane</string> <string name="mark_all_new_as_read">Zaznacz wszystkie nowe jako przeczytane</string> <string name="mark_all_as_read">Zaznacz wszystkie jako przeczytane</string> - <string name="account_backup_tips2_md"> ## Porady dotyczące kopii zapasowej i bezpieczeństwa - \n\nTwoje konto jest zabezpieczone tajnym kluczem. Kluczem jest długa sekwencja znaków, zaczynająca się od **nsec1**. Każdy, kto ma dostęp do tego tajnego klucza może publikować i zmieniać Twoją tożsamość. - \n\n- **nie** umiejszczaj swojego tajnego klucza na żadnej stronie internetowej lub oprogramowaniu, któremu nie ufasz. - \n- Deweloperzy Ametyst **nigdy** nie będą prosić o Twój tajny klucz. - \n- **Zrób ** bezpieczną kopię zapasową swojego tajnego klucza do ewentualnego odzyskania konta. Zalecamy użycie menedżera haseł. - </string> - <string name="account_backup_tips3_md"> Dla dodatkowego bezpieczeństwa możesz zaszyfrować swój klucz hasłem. Ten klucz zaczyna się od **ncryptsec1** i nie może być użyty bez hasła. - \n\nJeśli zgubisz hasło, nie będziesz w stanie odzyskać swojego klucza. - </string> + <string name="account_backup_tips2_md">## Porady dotyczące kopii zapasowej i bezpieczeństwa \n\nTwoje konto jest zabezpieczone tajnym kluczem. Kluczem jest długa sekwencja znaków, zaczynająca się od **nsec1**. Każdy, kto ma dostęp do tego tajnego klucza może publikować i zmieniać Twoją tożsamość. \n\n- **nie** umiejszczaj swojego tajnego klucza na żadnej stronie internetowej lub oprogramowaniu, któremu nie ufasz. \n- Deweloperzy Ametyst **nigdy** nie będą prosić o Twój tajny klucz. \n- **Zrób ** bezpieczną kopię zapasową swojego tajnego klucza do ewentualnego odzyskania konta. Zalecamy użycie menedżera haseł.</string> + <string name="account_backup_tips3_md">Dla dodatkowego bezpieczeństwa możesz zaszyfrować swój klucz hasłem. Ten klucz zaczyna się od **ncryptsec1** i nie może być użyty bez hasła. \n\nJeśli zgubisz hasło, nie będziesz w stanie odzyskać swojego klucza.</string> <string name="backup_keys_nudge_title">Kopia zapasowa kluczy</string> <string name="backup_keys_nudge_body">Twój tajny klucz jest jedynym sposobem na dostęp do tego konta. Jeśli go zgubisz, nigdy nie będzie można go odzyskać. Zapisz go w bezpiecznym miejscu.</string> <string name="backup_keys_nudge_backup_now">Utwórz kopię zapasową</string> @@ -1849,9 +1841,7 @@ <string name="push_server_none_explainer">Wyłącz powiadomienia</string> <string name="push_server_uses_app_explainer">Używa aplikacji %1$s</string> <string name="push_server_install_app">Ustawienia powiadomień</string> - <string name="push_server_install_app_description"> Aby otrzymywać powiadomienia, zainstaluj dowolną aplikację, która obsługuje [Unified Push](https://unifiedpush. rg/), np. [Ntfy](https://ntfy.sh/). - Po zainstalowaniu, wybierz w ustawieniach aplikację, którą chcesz użyć. - </string> + <string name="push_server_install_app_description">Aby otrzymywać powiadomienia, zainstaluj dowolną aplikację, która obsługuje [Unified Push](https://unifiedpush. rg/), np. [Ntfy](https://ntfy.sh/). Po zainstalowaniu, wybierz w ustawieniach aplikację, którą chcesz użyć.</string> <string name="thread_title">Wątek</string> <string name="send_the_seller_a_message">Wyślij sprzedawcy wiadomość</string> <string name="hi_there_is_this_still_available">Cześć, czy to jest nadal dostępne?</string> @@ -3656,15 +3646,7 @@ <string name="could_not_fetch_invoice_from_details">Nie można pobrać faktury od %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Nie można otworzyć skompresowanego pliku</string> <string name="couldnt_find_nwc_wallets">Nie znaleziono portfela</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst nie znalazł żadnej aplikacji portfela obsługującej Nostr Wallet Connect (NWC). -\n\nCo możesz zrobić: -\n\t1. Zweryfikuj wsparcie NWC: Sprawdź, czy preferowana aplikacja portfela jest kompatybilna z Nostr Wallet Connect. -\n\t2. Utwórz połączenie: Przejdź do aplikacji portfela i utwórz nowe połączenie NWC. -\n\t3. Połącz z Amethyst: Twój portfel zazwyczaj oferuje trzy opcje: -\n\t\t3. Automatyczne połączenie: przycisk, który automatycznie otwiera i łączy się z Amethyst. Jeśli tak, kliknij ten przycisk, otwórz Ametyst i naciśnij przycisk "Zapisz" na nowym ekranie. -\n\t\t3. Połączenie ręczne: adres URI połączenia, który można skopiować i wkleić, klikając ikonę wklejania na tym ekranie, aby wprowadzić go bezpośrednio. -\n\t\t3. Kod QR: Portfel tworzy kod QR, który można odczytać z przycisku QR na tym ekranie. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst nie znalazł żadnej aplikacji portfela obsługującej Nostr Wallet Connect (NWC). \n\nCo możesz zrobić: \n\t1. Zweryfikuj wsparcie NWC: Sprawdź, czy preferowana aplikacja portfela jest kompatybilna z Nostr Wallet Connect. \n\t2. Utwórz połączenie: Przejdź do aplikacji portfela i utwórz nowe połączenie NWC. \n\t3. Połącz z Amethyst: Twój portfel zazwyczaj oferuje trzy opcje: \n\t\t3. Automatyczne połączenie: przycisk, który automatycznie otwiera i łączy się z Amethyst. Jeśli tak, kliknij ten przycisk, otwórz Ametyst i naciśnij przycisk "Zapisz" na nowym ekranie. \n\t\t3. Połączenie ręczne: adres URI połączenia, który można skopiować i wkleić, klikając ikonę wklejania na tym ekranie, aby wprowadzić go bezpośrednio. \n\t\t3. Kod QR: Portfel tworzy kod QR, który można odczytać z przycisku QR na tym ekranie.</string> <string name="create">Utwórz</string> <string name="d">d</string> <string name="danger_zone">Niebezpieczna strefa</string> @@ -4966,10 +4948,7 @@ <string name="relay_auth_search_keywords">auth uwierzytelnianie transmiter podpis weryfikacja nip-42 tożsamość</string> <string name="relay_auth_settings_title">Uwierzytelnianie transmitera</string> <string name="relay_chat">Czat Transmitera</string> - <string name="relay_chat_explainer">Czaty transmiterów to grupy czatów kontrolowane przez ich macierzyste transmitery. - Są one widoczne dla wszystkich użytkowników Nostr i każdy może w nich uczestniczyć. - Świetnie nadają się do tworzenia otwartych społeczności wokół określonych tematów. Niektóre z tych grup są efemeryczne - i dlatego wiadomości czatu znikają z czasem</string> + <string name="relay_chat_explainer">Czaty transmiterów to grupy czatów kontrolowane przez ich macierzyste transmitery. Są one widoczne dla wszystkich użytkowników Nostr i każdy może w nich uczestniczyć. Świetnie nadają się do tworzenia otwartych społeczności wokół określonych tematów. Niektóre z tych grup są efemeryczne i dlatego wiadomości czatu znikają z czasem</string> <string name="relay_chat_title">Czaty Transmitera</string> <string name="relay_explain_account_data">Twój profil, ustawienia i wersje robocze na Twoich domowych transmiterach.</string> <string name="relay_explain_community_chats">Transmitery, na których każda społeczność publikuje swoje plany.</string> @@ -5178,8 +5157,7 @@ <string name="route_podcasts">Podcasty</string> <string name="schedule_post_button_add">Zaplanuj post</string> <string name="schedule_post_button_remove">Anuluj planowanie</string> - <string name="schedule_post_warning_multi">Wpisy zaplanowane mogą nie zostać opublikowane, dopóki nie uruchomisz ponownie aplikacji. -Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest aktywne. Włącz opcję „Zawsze włączone” w menu Ustawienia → Preferencje interfejsu użytkownika, aby zapewnić niezawodne planowanie zadań w tle.</string> + <string name="schedule_post_warning_multi">Wpisy zaplanowane mogą nie zostać opublikowane, dopóki nie uruchomisz ponownie aplikacji. Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest aktywne. Włącz opcję „Zawsze włączone” w menu Ustawienia → Preferencje interfejsu użytkownika, aby zapewnić niezawodne planowanie zadań w tle.</string> <string name="schedule_post_warning_single">Zaplanowane posty mogą nie zostać opublikowane, dopóki nie uruchomisz ponownie aplikacji. Aby zapewnić niezawodne planowanie zadań w tle, włącz opcję „Zawsze włączone” w Ustawieniach → Preferencje dotyczące niezawodnego planowania zadań w tle.</string> <string name="scheduled_posts">Zaplanowane posty</string> <string name="scheduled_posts_action_send_now">Wyślij teraz</string> @@ -5792,7 +5770,7 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest <item quantity="other">%1$d sposoby płatności zostały usunięte</item> </plurals> <string name="backup_review_payments_explainer">Osoby, które Ci płacą, widzą tylko to, co znajduje się na tej liście.</string> - <string name="backup_review_places_gone"> miejsca zniknęły</string> + <string name="backup_review_places_gone">miejsca zniknęły</string> <string name="backup_review_places_you_follow">Miejsca które obserwujesz</string> <string name="backup_review_private_added_body">Są teraz zaszyfrowane, co prawdopodobnie wyjaśnia, gdzie podziały się powyższe elementy publiczne.</string> <string name="backup_review_private_added_title">Ustawiłeś te pozycje jako prywatne</string> diff --git a/commonsUI/src/commonMain/composeResources/values-pt-rBR/strings.xml b/commonsUI/src/commonMain/composeResources/values-pt-rBR/strings.xml index 19715d92c0..1a156efffd 100644 --- a/commonsUI/src/commonMain/composeResources/values-pt-rBR/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-pt-rBR/strings.xml @@ -361,7 +361,7 @@ <string name="copy_user_id">Copiar ID do usuário</string> <string name="unblock_user">Desbloquear Usuário</string> <string name="user_is_blocked_hidden">Este usuário está bloqueado/silenciado. Suas publicações estão ocultas.</string> - <string name="npub_hex_username">npub, hex, usuário </string> + <string name="npub_hex_username">npub, hex, usuário</string> <string name="clear">Limpar</string> <string name="app_logo">Logo do aplicativo</string> <string name="nsec_npub_hex_private_key">nsec / npub / hex chave privada</string> @@ -449,8 +449,8 @@ <string name="paid_relay">Retransmissor pago</string> <string name="tor_relay">Forçar uso do Tor ao conectar</string> <string name="posts_received">postagens recebidas</string> - <string name="translations_auto">Autotraduzido </string> - <string name="translations_translated_from"> de </string> + <string name="translations_auto">Autotraduzido</string> + <string name="translations_translated_from">de</string> <string name="translations_to">para</string> <string name="translations_show_in_lang_first">Mostrar em %1$s primeiro</string> <string name="chat_about_topic">Chat público sobre %1$s</string> @@ -463,13 +463,8 @@ <string name="mark_all_known_as_read">Marcar todos conhecidos como lidos</string> <string name="mark_all_new_as_read">Marcar todas novas solicitações como lidas</string> <string name="mark_all_as_read">Marcar todas como lidas</string> - <string name="account_backup_tips2_md"> ## Dicas de Backup de Chave e Segurança - \n\nSua conta é protegida por uma chave secreta. A chave é uma sequência longa de caracteres que começa com **nsec1**. Qualquer pessoa que tenha acesso a esta chave secreta pode postar e alterar sua identidade. - \n\n- **Não** coloque sua chave secreta em nenhum site ou software que você não confie. - \n- Os desenvolvedores da Amethyst **nunca** pedirão sua chave secreta. - \n- **Mantenha** uma cópia de segurança segura de sua chave secreta para recuperação da conta. Recomendamos o uso de um gerenciador de senhas.</string> - <string name="account_backup_tips3_md"> Para segurança adicional, você pode criptografar sua chave com uma senha. Esta chave começa com **ncryptsec1** e não pode ser usada sem sua senha. - \n\nSe você perder sua senha, não será possível recuperar sua chave.</string> + <string name="account_backup_tips2_md">## Dicas de Backup de Chave e Segurança \n\nSua conta é protegida por uma chave secreta. A chave é uma sequência longa de caracteres que começa com **nsec1**. Qualquer pessoa que tenha acesso a esta chave secreta pode postar e alterar sua identidade. \n\n- **Não** coloque sua chave secreta em nenhum site ou software que você não confie. \n- Os desenvolvedores da Amethyst **nunca** pedirão sua chave secreta. \n- **Mantenha** uma cópia de segurança segura de sua chave secreta para recuperação da conta. Recomendamos o uso de um gerenciador de senhas.</string> + <string name="account_backup_tips3_md">Para segurança adicional, você pode criptografar sua chave com uma senha. Esta chave começa com **ncryptsec1** e não pode ser usada sem sua senha. \n\nSe você perder sua senha, não será possível recuperar sua chave.</string> <string name="backup_keys_nudge_title">Faça backup das suas chaves</string> <string name="backup_keys_nudge_body">Sua chave secreta é a única forma de acessar esta conta. Se você perdê-la, ela nunca poderá ser recuperada. Guarde-a em um lugar seguro agora.</string> <string name="backup_keys_nudge_backup_now">Fazer backup agora</string> @@ -1584,9 +1579,7 @@ <string name="push_server_none_explainer">Desativa as Notificações Push</string> <string name="push_server_uses_app_explainer">Usa o aplicativo %1$s</string> <string name="push_server_install_app">Configuração de Notificações Push</string> - <string name="push_server_install_app_description"> Para receber notificações push, instale qualquer aplicativo que suporte [Unified Push](https://unifiedpush.org/), como [Ntfy](https://ntfy.sh/). - Após a instalação, selecione o aplicativo que deseja usar nas configurações. - </string> + <string name="push_server_install_app_description">Para receber notificações push, instale qualquer aplicativo que suporte [Unified Push](https://unifiedpush.org/), como [Ntfy](https://ntfy.sh/). Após a instalação, selecione o aplicativo que deseja usar nas configurações.</string> <string name="thread_title">Linha</string> <string name="send_the_seller_a_message">Envie uma mensagem para o vendedor</string> <string name="hi_there_is_this_still_available">Olá, ainda está disponível?</string> @@ -3323,14 +3316,7 @@ <string name="could_not_fetch_invoice_from_details">Não foi possível obter a fatura de %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Não foi possível abrir o arquivo compactado</string> <string name="couldnt_find_nwc_wallets">Carteira não encontrada</string> - <string name="couldnt_find_nwc_wallets_description">O Amethyst não conseguiu encontrar nenhum aplicativo de carteira que suporte Nostr Wallet Connect (NWC). -\n\nO que você pode fazer: -\n\t1. Verifique o suporte NWC: Confirme se o aplicativo da sua carteira é compatível com o Nostr Wallet Connect. -\n\t2. Criar conexão: Vá até o aplicativo da sua carteira e crie uma nova conexão NWC. -\n\t3. Vincular ao Amethyst: Sua carteira geralmente oferece três opções: -\n\t\t3.1 Link Automático: Um botão para abrir e conectar automaticamente ao Amethyst. Clique nele, abra o Amethyst e salve na nova tela. -\n\t\t3.2 Link Manual: Um URI de conexão que você pode copiar e colar usando o ícone de colar nesta tela. -\n\t\t3.3 Código QR: A carteira gera um código QR que você pode ler usando o botão de código QR nesta tela.</string> + <string name="couldnt_find_nwc_wallets_description">O Amethyst não conseguiu encontrar nenhum aplicativo de carteira que suporte Nostr Wallet Connect (NWC). \n\nO que você pode fazer: \n\t1. Verifique o suporte NWC: Confirme se o aplicativo da sua carteira é compatível com o Nostr Wallet Connect. \n\t2. Criar conexão: Vá até o aplicativo da sua carteira e crie uma nova conexão NWC. \n\t3. Vincular ao Amethyst: Sua carteira geralmente oferece três opções: \n\t\t3.1 Link Automático: Um botão para abrir e conectar automaticamente ao Amethyst. Clique nele, abra o Amethyst e salve na nova tela. \n\t\t3.2 Link Manual: Um URI de conexão que você pode copiar e colar usando o ícone de colar nesta tela. \n\t\t3.3 Código QR: A carteira gera um código QR que você pode ler usando o botão de código QR nesta tela.</string> <string name="create">Criar</string> <string name="d">d</string> <string name="danger_zone">Zona de Perigo</string> @@ -4427,11 +4413,8 @@ <string name="proxy_section">Relays proxy</string> <string name="proxy_section_explainer">Relays agregadores que o app usa para baixar seus feeds, como filter.nostr.wine. Isso substitui o modelo outbox e faz o app se conectar apenas aos relays das suas listas.</string> <string name="public_chat">Chat público</string> - <string name="public_chat_explainer">Os chats públicos são visíveis para todos no Nostr, e qualquer pessoa - pode participar deles. Eles são ótimos para comunidades abertas em torno de tópicos específicos. - A moderação pode ser controlada excluindo postagens em seus relays</string> - <string name="public_chat_relays_explainer">Inserir entre 1–3 relés que hospedam esse grupo. - os clientes Nostr usam essa configuração para saber de onde fazer o download de mensagens e enviar suas mensagens.</string> + <string name="public_chat_explainer">Os chats públicos são visíveis para todos no Nostr, e qualquer pessoa pode participar deles. Eles são ótimos para comunidades abertas em torno de tópicos específicos. A moderação pode ser controlada excluindo postagens em seus relays</string> + <string name="public_chat_relays_explainer">Inserir entre 1–3 relés que hospedam esse grupo. os clientes Nostr usam essa configuração para saber de onde fazer o download de mensagens e enviar suas mensagens.</string> <string name="public_chat_relays_title">Relés</string> <string name="public_chat_title">Metadados do Chat Público</string> <string name="public_chats">Chats públicos</string> @@ -4496,10 +4479,7 @@ <string name="relay_auth_search_keywords">auth autenticação relay assinar verificar nip-42 identidade</string> <string name="relay_auth_settings_title">Autenticação de relay</string> <string name="relay_chat">Chat Relé</string> - <string name="relay_chat_explainer">Chat Relés são grupos de chat controlados por seu relé de origem. - Eles são visíveis para todos no Nostr e qualquer pessoa pode participar. - São ótimos para comunidades abertas em torno de tópicos específicos. Alguns desses grupos são efêmeros, - portanto, as mensagens desaparecem com o tempo</string> + <string name="relay_chat_explainer">Chat Relés são grupos de chat controlados por seu relé de origem. Eles são visíveis para todos no Nostr e qualquer pessoa pode participar. São ótimos para comunidades abertas em torno de tópicos específicos. Alguns desses grupos são efêmeros, portanto, as mensagens desaparecem com o tempo</string> <string name="relay_chat_title">Chat Relés</string> <string name="relay_explain_account_data">Seu próprio perfil, configurações e rascunhos, nos seus relays de origem.</string> <string name="relay_explain_community_chats">Os relays em que cada comunidade publica seus planos.</string> diff --git a/commonsUI/src/commonMain/composeResources/values-pt-rPT/strings.xml b/commonsUI/src/commonMain/composeResources/values-pt-rPT/strings.xml index b1953ebdcd..d722a0814b 100644 --- a/commonsUI/src/commonMain/composeResources/values-pt-rPT/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-pt-rPT/strings.xml @@ -223,7 +223,7 @@ <string name="copy_user_id">Copiar ID do usuário</string> <string name="unblock_user">Desbloquear Usuário</string> <string name="user_is_blocked_hidden">Este usuário está bloqueado/silenciado. Suas publicações estão ocultas.</string> - <string name="npub_hex_username">npub, hex, usuário </string> + <string name="npub_hex_username">npub, hex, usuário</string> <string name="clear">Limpar</string> <string name="app_logo">Logo do aplicativo</string> <string name="nsec_npub_hex_private_key">nsec / npub / hex chave privada</string> @@ -257,8 +257,8 @@ <string name="paid_relay">Retransmissor pago</string> <string name="tor_relay">Forçar uso do Tor ao conectar</string> <string name="posts_received">postagens recebidas</string> - <string name="translations_auto">Autotraduzido </string> - <string name="translations_translated_from"> de </string> + <string name="translations_auto">Autotraduzido</string> + <string name="translations_translated_from">de</string> <string name="translations_to">para</string> <string name="translations_show_in_lang_first">Mostrar em %1$s primeiro</string> <string name="chat_about_topic">Chat público sobre %1$s</string> @@ -271,13 +271,8 @@ <string name="mark_all_known_as_read">Marcar todos conhecidos como lidos</string> <string name="mark_all_new_as_read">Marcar todas novas solicitações como lidas</string> <string name="mark_all_as_read">Marcar todas como lidas</string> - <string name="account_backup_tips2_md"> ## Dicas de Backup de Chave e Segurança - \n\nSua conta é protegida por uma chave secreta. A chave é uma sequência longa de caracteres que começa com **nsec1**. Qualquer pessoa que tenha acesso a esta chave secreta pode postar e alterar sua identidade. - \n\n- **Não** coloque sua chave secreta em nenhum site ou software que você não confie. - \n- Os desenvolvedores da Amethyst **nunca** pedirão sua chave secreta. - \n- **Mantenha** uma cópia de segurança segura de sua chave secreta para recuperação da conta. Recomendamos o uso de um gerenciador de senhas.</string> - <string name="account_backup_tips3_md"> Para segurança adicional, você pode criptografar sua chave com uma senha. Esta chave começa com **ncryptsec1** e não pode ser usada sem sua senha. - \n\nSe você perder sua senha, não será possível recuperar sua chave.</string> + <string name="account_backup_tips2_md">## Dicas de Backup de Chave e Segurança \n\nSua conta é protegida por uma chave secreta. A chave é uma sequência longa de caracteres que começa com **nsec1**. Qualquer pessoa que tenha acesso a esta chave secreta pode postar e alterar sua identidade. \n\n- **Não** coloque sua chave secreta em nenhum site ou software que você não confie. \n- Os desenvolvedores da Amethyst **nunca** pedirão sua chave secreta. \n- **Mantenha** uma cópia de segurança segura de sua chave secreta para recuperação da conta. Recomendamos o uso de um gerenciador de senhas.</string> + <string name="account_backup_tips3_md">Para segurança adicional, você pode criptografar sua chave com uma senha. Esta chave começa com **ncryptsec1** e não pode ser usada sem sua senha. \n\nSe você perder sua senha, não será possível recuperar sua chave.</string> <string name="encrypt_and_copy_my_secret_key">Criptografar e copiar minha chave secreta</string> <string name="badge_award_image_for">Imagem da medalha para %1$s</string> <string name="badge_award_image">Imagem do prêmio</string> @@ -958,9 +953,7 @@ <string name="push_server_none_explainer">Desativa as Notificações Push</string> <string name="push_server_uses_app_explainer">Usa o aplicativo %1$s</string> <string name="push_server_install_app">Configuração de Notificações Push</string> - <string name="push_server_install_app_description"> Para receber notificações push, instale qualquer aplicativo que suporte [Unified Push](https://unifiedpush.org/), como [Ntfy](https://ntfy.sh/). - Após a instalação, selecione o aplicativo que deseja usar nas configurações. - </string> + <string name="push_server_install_app_description">Para receber notificações push, instale qualquer aplicativo que suporte [Unified Push](https://unifiedpush.org/), como [Ntfy](https://ntfy.sh/). Após a instalação, selecione o aplicativo que deseja usar nas configurações.</string> <string name="thread_title">Linha</string> <string name="send_the_seller_a_message">Envie uma mensagem para o vendedor</string> <string name="hi_there_is_this_still_available">Olá, ainda está disponível?</string> @@ -2003,14 +1996,7 @@ <string name="could_not_fetch_invoice_from_details">Não foi possível obter a fatura de %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Não foi possível abrir o arquivo compactado</string> <string name="couldnt_find_nwc_wallets">Carteira não encontrada</string> - <string name="couldnt_find_nwc_wallets_description">O Amethyst não conseguiu encontrar nenhum aplicativo de carteira que suporte Nostr Wallet Connect (NWC). -\n\nO que você pode fazer: -\n\t1. Verifique o suporte NWC: Confirme se o aplicativo da sua carteira é compatível com o Nostr Wallet Connect. -\n\t2. Criar conexão: Vá até o aplicativo da sua carteira e crie uma nova conexão NWC. -\n\t3. Vincular ao Amethyst: Sua carteira geralmente oferece três opções: -\n\t\t3.1 Link Automático: Um botão para abrir e conectar automaticamente ao Amethyst. Clique nele, abra o Amethyst e salve na nova tela. -\n\t\t3.2 Link Manual: Um URI de conexão que você pode copiar e colar usando o ícone de colar nesta tela. -\n\t\t3.3 Código QR: A carteira gera um código QR que você pode ler usando o botão de código QR nesta tela.</string> + <string name="couldnt_find_nwc_wallets_description">O Amethyst não conseguiu encontrar nenhum aplicativo de carteira que suporte Nostr Wallet Connect (NWC). \n\nO que você pode fazer: \n\t1. Verifique o suporte NWC: Confirme se o aplicativo da sua carteira é compatível com o Nostr Wallet Connect. \n\t2. Criar conexão: Vá até o aplicativo da sua carteira e crie uma nova conexão NWC. \n\t3. Vincular ao Amethyst: Sua carteira geralmente oferece três opções: \n\t\t3.1 Link Automático: Um botão para abrir e conectar automaticamente ao Amethyst. Clique nele, abra o Amethyst e salve na nova tela. \n\t\t3.2 Link Manual: Um URI de conexão que você pode copiar e colar usando o ícone de colar nesta tela. \n\t\t3.3 Código QR: A carteira gera um código QR que você pode ler usando o botão de código QR nesta tela.</string> <string name="create">Criar</string> <string name="danger_zone">Zona de Perigo</string> <string name="dark">Escuro</string> @@ -2662,11 +2648,8 @@ <string name="proxy_section">Relays proxy</string> <string name="proxy_section_explainer">Relays agregadores que o app usa para baixar seus feeds, como filter.nostr.wine. Isso substitui o modelo outbox e faz o app se conectar apenas aos relays das suas listas.</string> <string name="public_chat">Chat público</string> - <string name="public_chat_explainer">Os chats públicos são visíveis para todos no Nostr, e qualquer pessoa - pode participar deles. Eles são ótimos para comunidades abertas em torno de tópicos específicos. - A moderação pode ser controlada excluindo postagens em seus relays</string> - <string name="public_chat_relays_explainer">Inserir entre 1–3 relés que hospedam esse grupo. - os clientes Nostr usam essa configuração para saber de onde fazer o download de mensagens e enviar suas mensagens.</string> + <string name="public_chat_explainer">Os chats públicos são visíveis para todos no Nostr, e qualquer pessoa pode participar deles. Eles são ótimos para comunidades abertas em torno de tópicos específicos. A moderação pode ser controlada excluindo postagens em seus relays</string> + <string name="public_chat_relays_explainer">Inserir entre 1–3 relés que hospedam esse grupo. os clientes Nostr usam essa configuração para saber de onde fazer o download de mensagens e enviar suas mensagens.</string> <string name="public_chat_relays_title">Relés</string> <string name="public_chat_title">Metadados do Chat Público</string> <string name="public_chats">Chats públicos</string> @@ -2691,10 +2674,7 @@ <string name="read_only_user">Usuário somente leitura</string> <string name="record_a_message_description">Clique e segure para gravar uma mensagem</string> <string name="relay_chat">Chat Relé</string> - <string name="relay_chat_explainer">Chat Relés são grupos de chat controlados por seu relé de origem. - Eles são visíveis para todos no Nostr e qualquer pessoa pode participar. - São ótimos para comunidades abertas em torno de tópicos específicos. Alguns desses grupos são efêmeros, - portanto, as mensagens desaparecem com o tempo</string> + <string name="relay_chat_explainer">Chat Relés são grupos de chat controlados por seu relé de origem. Eles são visíveis para todos no Nostr e qualquer pessoa pode participar. São ótimos para comunidades abertas em torno de tópicos específicos. Alguns desses grupos são efêmeros, portanto, as mensagens desaparecem com o tempo</string> <string name="relay_chat_title">Chat Relés</string> <string name="relay_management_tab_events">Eventos</string> <string name="relay_management_tab_kinds">Tipos</string> diff --git a/commonsUI/src/commonMain/composeResources/values-ru-rRU/strings.xml b/commonsUI/src/commonMain/composeResources/values-ru-rRU/strings.xml index 42c411c558..d89352fd93 100644 --- a/commonsUI/src/commonMain/composeResources/values-ru-rRU/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ru-rRU/strings.xml @@ -226,7 +226,7 @@ <string name="copy_user_id">Копировать ID пользователя</string> <string name="unblock_user">Разблокировать</string> <string name="user_is_blocked_hidden">Этот пользователь заблокирован/заглушён. Его посты скрыты.</string> - <string name="npub_hex_username">npub, hex, никнейм </string> + <string name="npub_hex_username">npub, hex, никнейм</string> <string name="clear">Очистить</string> <string name="app_logo">Логотип приложения</string> <string name="nsec_npub_hex_private_key">nsec / npub / приватный ключ в hex</string> @@ -274,15 +274,8 @@ <string name="mark_all_known_as_read">Отметить все известные прочитанными</string> <string name="mark_all_new_as_read">Отметить все новые прочитанными</string> <string name="mark_all_as_read">Отметить всё прочитанным</string> - <string name="account_backup_tips2_md"> ## Советы по резервному копированию и безопасности ключей - \n\nВаш аккаунт защищён секретным ключом. Ключ — это длинная последовательность символов, начинающаяся с **nsec1**. Любой, кто получит доступ к этому секретному ключу, сможет публиковать записи и менять вашу личность. - \n\n- **Не** вводите свой секретный ключ на сайтах или в приложениях, которым не доверяете. - \n- Разработчики Amethyst **никогда** не будут запрашивать ваш секретный ключ. - \n- **Обязательно** сохраните надёжную резервную копию секретного ключа для восстановления аккаунта. Рекомендуем использовать менеджер паролей. - </string> - <string name="account_backup_tips3_md"> Для дополнительной безопасности вы можете зашифровать ваш ключ с помощью пароля. Этот ключ начинается с **ncryptsec1** и не может быть использован без пароля. - \n\nЕсли вы потеряете свой пароль, вы не сможете восстановить ваш ключ. - </string> + <string name="account_backup_tips2_md">## Советы по резервному копированию и безопасности ключей \n\nВаш аккаунт защищён секретным ключом. Ключ — это длинная последовательность символов, начинающаяся с **nsec1**. Любой, кто получит доступ к этому секретному ключу, сможет публиковать записи и менять вашу личность. \n\n- **Не** вводите свой секретный ключ на сайтах или в приложениях, которым не доверяете. \n- Разработчики Amethyst **никогда** не будут запрашивать ваш секретный ключ. \n- **Обязательно** сохраните надёжную резервную копию секретного ключа для восстановления аккаунта. Рекомендуем использовать менеджер паролей.</string> + <string name="account_backup_tips3_md">Для дополнительной безопасности вы можете зашифровать ваш ключ с помощью пароля. Этот ключ начинается с **ncryptsec1** и не может быть использован без пароля. \n\nЕсли вы потеряете свой пароль, вы не сможете восстановить ваш ключ.</string> <string name="encrypt_and_copy_my_secret_key">Зашифровать и копировать мой секретный ключ</string> <string name="badge_award_image_for">Картинка значка-награды %1$s</string> <string name="badge_award_image">Изображение награды за значок</string> @@ -970,9 +963,7 @@ <string name="push_server_none_explainer">Отключает push-уведомления</string> <string name="push_server_uses_app_explainer">Использует приложение %1$s</string> <string name="push_server_install_app">Настройки push-уведомлений</string> - <string name="push_server_install_app_description"> Для получения push-уведомлений, установите приложение, которое поддерживает [Unified Push](https://unifiedpush.org/), например [Ntfy](https://ntfy.sh/). - После установки выберите приложение, которое хотите использовать, в настройках. - </string> + <string name="push_server_install_app_description">Для получения push-уведомлений, установите приложение, которое поддерживает [Unified Push](https://unifiedpush.org/), например [Ntfy](https://ntfy.sh/). После установки выберите приложение, которое хотите использовать, в настройках.</string> <string name="thread_title">Ветка</string> <string name="send_the_seller_a_message">Написать продавцу</string> <string name="hi_there_is_this_still_available">Привет, это ещё в наличии?</string> @@ -2046,15 +2037,7 @@ <string name="could_not_fetch_invoice_from_details">Не удалось получить счёт от %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Не удалось открыть сжатый файл</string> <string name="couldnt_find_nwc_wallets">Кошелёк не найден</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst не нашёл ни одного приложения кошелька с поддержкой Nostr Wallet Connect (NWC). -\n\nЧто можно сделать: -\n\t1. Проверьте поддержку NWC: убедитесь, что ваш кошелёк совместим с Nostr Wallet Connect. -\n\t2. Создайте подключение: откройте приложение кошелька и создайте новое NWC-подключение. -\n\t3. Привяжите к Amethyst: обычно кошелёк предлагает три варианта: -\n\t\t3.1 Автоматическая привязка: кнопка для автоматического открытия и подключения к Amethyst. Нажмите её, откройте Amethyst и сохраните новый экран. -\n\t\t3.2 Ручная привязка: URI подключения, который можно скопировать и вставить с помощью значка вставки на этом экране. -\n\t\t3.3 QR-код: кошелёк создаёт QR-код, который можно считать кнопкой QR-кода на этом экране. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst не нашёл ни одного приложения кошелька с поддержкой Nostr Wallet Connect (NWC). \n\nЧто можно сделать: \n\t1. Проверьте поддержку NWC: убедитесь, что ваш кошелёк совместим с Nostr Wallet Connect. \n\t2. Создайте подключение: откройте приложение кошелька и создайте новое NWC-подключение. \n\t3. Привяжите к Amethyst: обычно кошелёк предлагает три варианта: \n\t\t3.1 Автоматическая привязка: кнопка для автоматического открытия и подключения к Amethyst. Нажмите её, откройте Amethyst и сохраните новый экран. \n\t\t3.2 Ручная привязка: URI подключения, который можно скопировать и вставить с помощью значка вставки на этом экране. \n\t\t3.3 QR-код: кошелёк создаёт QR-код, который можно считать кнопкой QR-кода на этом экране.</string> <string name="create">Создать</string> <string name="d">д</string> <string name="danger_zone">Опасная зона</string> @@ -2737,11 +2720,8 @@ <string name="propose_an_edit">Предложить изменение</string> <string name="proxy_section_explainer">Агрегирующие relay-серверы, которые приложение использует для загрузки ваших лент, например filter.nostr.wine. Это заменяет модель outbox и позволит приложению подключаться только к relay-серверам из ваших списков.</string> <string name="public_chat">Публичный чат</string> - <string name="public_chat_explainer">Публичные чаты видны всем в Nostr и открыты - для участия любого желающего. Они отлично подходят для открытых сообществ по определённым темам. - Модерация осуществляется путём удаления постов на relay</string> - <string name="public_chat_relays_explainer">Укажите от 1 до 3 relay, на которых размещена эта группа. - Клиенты Nostr используют эту настройку, чтобы знать, откуда загружать сообщения и куда их отправлять.</string> + <string name="public_chat_explainer">Публичные чаты видны всем в Nostr и открыты для участия любого желающего. Они отлично подходят для открытых сообществ по определённым темам. Модерация осуществляется путём удаления постов на relay</string> + <string name="public_chat_relays_explainer">Укажите от 1 до 3 relay, на которых размещена эта группа. Клиенты Nostr используют эту настройку, чтобы знать, откуда загружать сообщения и куда их отправлять.</string> <string name="public_chat_relays_title">Relay</string> <string name="public_chat_title">Метаданные публичного чата</string> <string name="public_chats">Публичные чаты</string> @@ -2766,10 +2746,7 @@ <string name="read_only_user">Пользователь только для чтения</string> <string name="record_a_message_description">Нажмите и удерживайте, чтобы записать сообщение</string> <string name="relay_chat">Чат relay</string> - <string name="relay_chat_explainer">Чаты relay — это групповые чаты, управляемые их домашним relay. - Они видны всем в Nostr, и любой может в них участвовать. - Они отлично подходят для открытых сообществ по определённым темам. Некоторые группы эфемерны, - поэтому сообщения исчезают со временем</string> + <string name="relay_chat_explainer">Чаты relay — это групповые чаты, управляемые их домашним relay. Они видны всем в Nostr, и любой может в них участвовать. Они отлично подходят для открытых сообществ по определённым темам. Некоторые группы эфемерны, поэтому сообщения исчезают со временем</string> <string name="relay_chat_title">Чаты relay</string> <string name="relay_management_tab_events">События</string> <string name="relay_management_tab_ips">IP-адреса</string> diff --git a/commonsUI/src/commonMain/composeResources/values-ru-rUA/strings.xml b/commonsUI/src/commonMain/composeResources/values-ru-rUA/strings.xml index f88269516e..4b2c8833b7 100644 --- a/commonsUI/src/commonMain/composeResources/values-ru-rUA/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ru-rUA/strings.xml @@ -267,15 +267,8 @@ <string name="mark_all_known_as_read">Отметить все известные как прочитанные</string> <string name="mark_all_new_as_read">Отметить все новые как прочитанные</string> <string name="mark_all_as_read">Отметить все как прочитанные</string> - <string name="account_backup_tips2_md"> ## Советы по резервному копированию и безопасности ключей - \n\nВаш аккаунт защищён секретным ключом. Ключ — это длинная последовательность символов, начинающаяся с **nsec1**. Любой, кто получит доступ к этому секретному ключу, сможет публиковать записи и менять вашу личность. - \n\n- **Не** вводите свой секретный ключ на сайтах или в приложениях, которым не доверяете. - \n- Разработчики Amethyst **никогда** не будут запрашивать ваш секретный ключ. - \n- **Обязательно** сохраните надёжную резервную копию секретного ключа для восстановления аккаунта. Рекомендуем использовать менеджер паролей. - </string> - <string name="account_backup_tips3_md"> Для дополнительной безопасности вы можете зашифровать ваш ключ паролем. Этот ключ начинается с **ncryptsec1** и не может быть использован без вашего пароля. - \n\nЕсли вы потеряете пароль, вы не сможете восстановить ключ. - </string> + <string name="account_backup_tips2_md">## Советы по резервному копированию и безопасности ключей \n\nВаш аккаунт защищён секретным ключом. Ключ — это длинная последовательность символов, начинающаяся с **nsec1**. Любой, кто получит доступ к этому секретному ключу, сможет публиковать записи и менять вашу личность. \n\n- **Не** вводите свой секретный ключ на сайтах или в приложениях, которым не доверяете. \n- Разработчики Amethyst **никогда** не будут запрашивать ваш секретный ключ. \n- **Обязательно** сохраните надёжную резервную копию секретного ключа для восстановления аккаунта. Рекомендуем использовать менеджер паролей.</string> + <string name="account_backup_tips3_md">Для дополнительной безопасности вы можете зашифровать ваш ключ паролем. Этот ключ начинается с **ncryptsec1** и не может быть использован без вашего пароля. \n\nЕсли вы потеряете пароль, вы не сможете восстановить ключ.</string> <string name="encrypt_and_copy_my_secret_key">Зашифровать и скопировать мой секретный ключ</string> <string name="badge_award_image_for">Изображение награды для %1$s</string> <string name="badge_award_image">Изображение награды за значок</string> @@ -960,9 +953,7 @@ <string name="push_server_none_explainer">Отключает push-уведомления</string> <string name="push_server_uses_app_explainer">Использует приложение %1$s</string> <string name="push_server_install_app">Настройка push-уведомлений</string> - <string name="push_server_install_app_description"> Для получения push-уведомлений установите любое приложение, поддерживающее [Unified Push](https://unifiedpush.org/), например [Ntfy](https://ntfy.sh/). - После установки выберите нужное приложение в Настройках. - </string> + <string name="push_server_install_app_description">Для получения push-уведомлений установите любое приложение, поддерживающее [Unified Push](https://unifiedpush.org/), например [Ntfy](https://ntfy.sh/). После установки выберите нужное приложение в Настройках.</string> <string name="thread_title">Тред</string> <string name="send_the_seller_a_message">Написать продавцу</string> <string name="hi_there_is_this_still_available">Привет, это ещё в наличии?</string> @@ -2032,15 +2023,7 @@ <string name="could_not_fetch_invoice_from_details">Не удалось получить счёт от %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Не удалось открыть сжатый файл</string> <string name="couldnt_find_nwc_wallets">Кошелёк не найден</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst не нашёл ни одного приложения кошелька с поддержкой Nostr Wallet Connect (NWC). -\n\nЧто можно сделать: -\n\t1. Проверьте поддержку NWC: убедитесь, что ваш кошелёк совместим с Nostr Wallet Connect. -\n\t2. Создайте подключение: откройте приложение кошелька и создайте новое NWC-подключение. -\n\t3. Привяжите к Amethyst: обычно кошелёк предлагает три варианта: -\n\t\t3.1 Автоматическая привязка: кнопка для автоматического открытия и подключения к Amethyst. Нажмите её, откройте Amethyst и сохраните новый экран. -\n\t\t3.2 Ручная привязка: URI подключения, который можно скопировать и вставить с помощью значка вставки на этом экране. -\n\t\t3.3 QR-код: кошелёк создаёт QR-код, который можно считать кнопкой QR-кода на этом экране. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst не нашёл ни одного приложения кошелька с поддержкой Nostr Wallet Connect (NWC). \n\nЧто можно сделать: \n\t1. Проверьте поддержку NWC: убедитесь, что ваш кошелёк совместим с Nostr Wallet Connect. \n\t2. Создайте подключение: откройте приложение кошелька и создайте новое NWC-подключение. \n\t3. Привяжите к Amethyst: обычно кошелёк предлагает три варианта: \n\t\t3.1 Автоматическая привязка: кнопка для автоматического открытия и подключения к Amethyst. Нажмите её, откройте Amethyst и сохраните новый экран. \n\t\t3.2 Ручная привязка: URI подключения, который можно скопировать и вставить с помощью значка вставки на этом экране. \n\t\t3.3 QR-код: кошелёк создаёт QR-код, который можно считать кнопкой QR-кода на этом экране.</string> <string name="create">Создать</string> <string name="danger_zone">Опасная зона</string> <string name="dark">Тёмная</string> @@ -2719,11 +2702,8 @@ <string name="propose_an_edit">Предложить правку</string> <string name="proxy_section_explainer">Агрегирующие relay-серверы, которые приложение использует для загрузки ваших лент, например filter.nostr.wine. Это заменяет модель outbox и позволит приложению подключаться только к relay-серверам из ваших списков.</string> <string name="public_chat">Публичный чат</string> - <string name="public_chat_explainer">Публичные чаты видны всем в Nostr и открыты - для участия любого желающего. Они отлично подходят для открытых сообществ по определённым темам. - Модерация осуществляется путём удаления постов на relay</string> - <string name="public_chat_relays_explainer">Укажите от 1 до 3 relay, на которых размещена эта группа. - Клиенты Nostr используют эту настройку, чтобы знать, откуда загружать сообщения и куда их отправлять.</string> + <string name="public_chat_explainer">Публичные чаты видны всем в Nostr и открыты для участия любого желающего. Они отлично подходят для открытых сообществ по определённым темам. Модерация осуществляется путём удаления постов на relay</string> + <string name="public_chat_relays_explainer">Укажите от 1 до 3 relay, на которых размещена эта группа. Клиенты Nostr используют эту настройку, чтобы знать, откуда загружать сообщения и куда их отправлять.</string> <string name="public_chat_relays_title">Relay</string> <string name="public_chat_title">Метаданные публичного чата</string> <string name="public_chats">Публичные чаты</string> @@ -2748,10 +2728,7 @@ <string name="read_only_user">Пользователь только для чтения</string> <string name="record_a_message_description">Нажмите и удерживайте для записи сообщения</string> <string name="relay_chat">Чат relay</string> - <string name="relay_chat_explainer">Чаты relay — это групповые чаты, управляемые их домашним relay. - Они видны всем в Nostr, и любой может в них участвовать. - Они отлично подходят для открытых сообществ по определённым темам. Некоторые группы эфемерны, - поэтому сообщения исчезают со временем</string> + <string name="relay_chat_explainer">Чаты relay — это групповые чаты, управляемые их домашним relay. Они видны всем в Nostr, и любой может в них участвовать. Они отлично подходят для открытых сообществ по определённым темам. Некоторые группы эфемерны, поэтому сообщения исчезают со временем</string> <string name="relay_chat_title">Чаты relay</string> <string name="relay_management_tab_events">События</string> <string name="relay_management_tab_ips">IP-адреса</string> diff --git a/commonsUI/src/commonMain/composeResources/values-ru/strings.xml b/commonsUI/src/commonMain/composeResources/values-ru/strings.xml index 5a3a0ea0eb..9ceaec1f2a 100644 --- a/commonsUI/src/commonMain/composeResources/values-ru/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ru/strings.xml @@ -215,7 +215,7 @@ <string name="copy_user_id">Копировать ID пользователя</string> <string name="unblock_user">Разблокировать</string> <string name="user_is_blocked_hidden">Этот пользователь заблокирован/заглушён. Его посты скрыты.</string> - <string name="npub_hex_username">npub, hex, никнейм </string> + <string name="npub_hex_username">npub, hex, никнейм</string> <string name="clear">Очистить</string> <string name="app_logo">Логотип приложения</string> <string name="nsec_npub_hex_private_key">nsec / npub / приватный ключ в hex</string> @@ -263,17 +263,8 @@ <string name="mark_all_known_as_read">Отметить все известные прочитанными</string> <string name="mark_all_new_as_read">Отметить все новые прочитанными</string> <string name="mark_all_as_read">Отметить всё прочитанным</string> - <string name="account_backup_tips2_md"> - ## Советы по резервному копированию и безопасности ключей - \n\nВаш аккаунт защищён секретным ключом. Ключ — это длинная последовательность символов, начинающаяся с **nsec1**. Любой, кто получит доступ к этому секретному ключу, сможет публиковать записи и менять вашу личность. - \n\n- **Не** вводите свой секретный ключ на сайтах или в приложениях, которым не доверяете. - \n- Разработчики Amethyst **никогда** не будут запрашивать ваш секретный ключ. - \n- **Обязательно** сохраните надёжную резервную копию секретного ключа для восстановления аккаунта. Рекомендуем использовать менеджер паролей. - </string> - <string name="account_backup_tips3_md"> - Для дополнительной безопасности вы можете зашифровать ваш ключ паролем. Этот ключ начинается с **ncryptsec1** и не может быть использован без вашего пароля. - \n\nЕсли вы потеряете пароль, вы не сможете восстановить ваш ключ. - </string> + <string name="account_backup_tips2_md">## Советы по резервному копированию и безопасности ключей \n\nВаш аккаунт защищён секретным ключом. Ключ — это длинная последовательность символов, начинающаяся с **nsec1**. Любой, кто получит доступ к этому секретному ключу, сможет публиковать записи и менять вашу личность. \n\n- **Не** вводите свой секретный ключ на сайтах или в приложениях, которым не доверяете. \n- Разработчики Amethyst **никогда** не будут запрашивать ваш секретный ключ. \n- **Обязательно** сохраните надёжную резервную копию секретного ключа для восстановления аккаунта. Рекомендуем использовать менеджер паролей.</string> + <string name="account_backup_tips3_md">Для дополнительной безопасности вы можете зашифровать ваш ключ паролем. Этот ключ начинается с **ncryptsec1** и не может быть использован без вашего пароля. \n\nЕсли вы потеряете пароль, вы не сможете восстановить ваш ключ.</string> <string name="encrypt_and_copy_my_secret_key">Зашифровать и скопировать секретный ключ</string> <string name="badge_award_image_for">Картинка значка-награды %1$s</string> <string name="badge_award_image">Изображение награды за значок</string> @@ -966,9 +957,7 @@ <string name="push_server_none_explainer">Отключает push-уведомления</string> <string name="push_server_uses_app_explainer">Использует приложение %1$s</string> <string name="push_server_install_app">Настройки push-уведомлений</string> - <string name="push_server_install_app_description"> Для получения push-уведомлений, установите приложение, которое поддерживает [Unified Push](https://unifiedpush.org/), например [Ntfy](https://ntfy.sh/). - После установки выберите приложение, которое хотите использовать, в настройках. - </string> + <string name="push_server_install_app_description">Для получения push-уведомлений, установите приложение, которое поддерживает [Unified Push](https://unifiedpush.org/), например [Ntfy](https://ntfy.sh/). После установки выберите приложение, которое хотите использовать, в настройках.</string> <string name="thread_title">Ветка</string> <string name="send_the_seller_a_message">Написать продавцу</string> <string name="hi_there_is_this_still_available">Привет, это ещё в наличии?</string> @@ -2077,15 +2066,7 @@ <string name="could_not_fetch_invoice_from_details">Не удалось получить счёт от %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Не удалось открыть сжатый файл</string> <string name="couldnt_find_nwc_wallets">Кошелёк не найден</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst не нашёл ни одного приложения кошелька с поддержкой Nostr Wallet Connect (NWC). -\n\nЧто можно сделать: -\n\t1. Проверьте поддержку NWC: убедитесь, что ваш кошелёк совместим с Nostr Wallet Connect. -\n\t2. Создайте подключение: откройте приложение кошелька и создайте новое NWC-подключение. -\n\t3. Привяжите к Amethyst: обычно кошелёк предлагает три варианта: -\n\t\t3.1 Автоматическая привязка: кнопка для автоматического открытия и подключения к Amethyst. Нажмите её, откройте Amethyst и сохраните новый экран. -\n\t\t3.2 Ручная привязка: URI подключения, который можно скопировать и вставить с помощью значка вставки на этом экране. -\n\t\t3.3 QR-код: кошелёк создаёт QR-код, который можно считать кнопкой QR-кода на этом экране. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst не нашёл ни одного приложения кошелька с поддержкой Nostr Wallet Connect (NWC). \n\nЧто можно сделать: \n\t1. Проверьте поддержку NWC: убедитесь, что ваш кошелёк совместим с Nostr Wallet Connect. \n\t2. Создайте подключение: откройте приложение кошелька и создайте новое NWC-подключение. \n\t3. Привяжите к Amethyst: обычно кошелёк предлагает три варианта: \n\t\t3.1 Автоматическая привязка: кнопка для автоматического открытия и подключения к Amethyst. Нажмите её, откройте Amethyst и сохраните новый экран. \n\t\t3.2 Ручная привязка: URI подключения, который можно скопировать и вставить с помощью значка вставки на этом экране. \n\t\t3.3 QR-код: кошелёк создаёт QR-код, который можно считать кнопкой QR-кода на этом экране.</string> <string name="create">Создать</string> <string name="d">д</string> <string name="danger_zone">Опасная зона</string> @@ -2815,11 +2796,8 @@ <string name="proxy_section">Proxy Relays</string> <string name="proxy_section_explainer">Агрегирующие relay-серверы, которые приложение использует для загрузки ваших лент, например filter.nostr.wine. Это заменяет модель outbox и позволит приложению подключаться только к relay-серверам из ваших списков.</string> <string name="public_chat">Публичный чат</string> - <string name="public_chat_explainer">Публичные чаты видны всем в Nostr и открыты - для участия любого желающего. Они отлично подходят для открытых сообществ по определённым темам. - Модерация осуществляется путём удаления постов на relay</string> - <string name="public_chat_relays_explainer">Укажите от 1 до 3 relay, на которых размещена эта группа. - Клиенты Nostr используют эту настройку, чтобы знать, откуда загружать сообщения и куда их отправлять.</string> + <string name="public_chat_explainer">Публичные чаты видны всем в Nostr и открыты для участия любого желающего. Они отлично подходят для открытых сообществ по определённым темам. Модерация осуществляется путём удаления постов на relay</string> + <string name="public_chat_relays_explainer">Укажите от 1 до 3 relay, на которых размещена эта группа. Клиенты Nostr используют эту настройку, чтобы знать, откуда загружать сообщения и куда их отправлять.</string> <string name="public_chat_relays_title">Relay</string> <string name="public_chat_title">Метаданные публичного чата</string> <string name="public_chats">Публичные чаты</string> @@ -2844,10 +2822,7 @@ <string name="read_only_user">Пользователь только для чтения</string> <string name="record_a_message_description">Нажмите и удерживайте для записи сообщения</string> <string name="relay_chat">Чат relay</string> - <string name="relay_chat_explainer">Чаты relay — это групповые чаты, управляемые их домашним relay. - Они видны всем в Nostr, и любой может в них участвовать. - Они отлично подходят для открытых сообществ по определённым темам. Некоторые группы эфемерны, - поэтому сообщения исчезают со временем</string> + <string name="relay_chat_explainer">Чаты relay — это групповые чаты, управляемые их домашним relay. Они видны всем в Nostr, и любой может в них участвовать. Они отлично подходят для открытых сообществ по определённым темам. Некоторые группы эфемерны, поэтому сообщения исчезают со временем</string> <string name="relay_chat_title">Чаты relay</string> <string name="relay_management_tab_events">События</string> <string name="relay_management_tab_ips">IP-адреса</string> diff --git a/commonsUI/src/commonMain/composeResources/values-sl-rSI/strings.xml b/commonsUI/src/commonMain/composeResources/values-sl-rSI/strings.xml index 2f5ff233bb..8ae6e95125 100644 --- a/commonsUI/src/commonMain/composeResources/values-sl-rSI/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-sl-rSI/strings.xml @@ -472,16 +472,8 @@ <string name="mark_all_known_as_read">Označi vse znane kot prebrane</string> <string name="mark_all_new_as_read">Označi vse nove kot prebrane</string> <string name="mark_all_as_read">Označi vse kot prebrane</string> - <string name="account_backup_tips2_md"> - ## Varnostna kopija ključev in varnostni nasveti - \n\nVaš račun je zaščiten z privatnim ključem. Ta ključ ima dolgo sekvenco znakov kateri se začnejo z **nsec1**. Kdorkoli ima dostop, do tega privatnega ključa, lahko pošiljajo zapiske in spremenijo vašo digitalno identiteto. - \n\n- Nikoli in **Nikdar** ne vpisuj svojega privatnega ključa v spletne strani in aplikacije katerim ne zaupaš. - \n- Amethyst ustvarjalci vas ne bojo **nikoli** vprašali po vašem privatnem ključu. - \n- Ustvari **varnostno kopijo** svojega privatnega ključa, za obnovitev računa. Priporoča se uporaba upravljalnika gesel. - </string> - <string name="account_backup_tips3_md"> Za dodatno varnost, lahko šifriraš svoj ključ z dodatnim geslom. Ta ključ se začne z **ncryptsec1** in ne more bit uporabljen brez dodatnega gesla. - \n\nČe izgubiš geslo, obnova ključa ni več mogoča. - </string> + <string name="account_backup_tips2_md">## Varnostna kopija ključev in varnostni nasveti \n\nVaš račun je zaščiten z privatnim ključem. Ta ključ ima dolgo sekvenco znakov kateri se začnejo z **nsec1**. Kdorkoli ima dostop, do tega privatnega ključa, lahko pošiljajo zapiske in spremenijo vašo digitalno identiteto. \n\n- Nikoli in **Nikdar** ne vpisuj svojega privatnega ključa v spletne strani in aplikacije katerim ne zaupaš. \n- Amethyst ustvarjalci vas ne bojo **nikoli** vprašali po vašem privatnem ključu. \n- Ustvari **varnostno kopijo** svojega privatnega ključa, za obnovitev računa. Priporoča se uporaba upravljalnika gesel.</string> + <string name="account_backup_tips3_md">Za dodatno varnost, lahko šifriraš svoj ključ z dodatnim geslom. Ta ključ se začne z **ncryptsec1** in ne more bit uporabljen brez dodatnega gesla. \n\nČe izgubiš geslo, obnova ključa ni več mogoča.</string> <string name="backup_keys_nudge_title">Varno shrani ključe</string> <string name="backup_keys_nudge_body">Vaš zasebni ključ je edini način za dostop do tega računa. Če ga izgubite, ga ne boste mogli več obnoviti. Shranite ga na varno mesto.</string> <string name="backup_keys_nudge_backup_now">Varno shrani zdaj</string> @@ -1593,9 +1585,7 @@ <string name="push_server_none_explainer">Onemogoči potisna obvestila</string> <string name="push_server_uses_app_explainer">Uporablja aplikacijo %1$s</string> <string name="push_server_install_app">Nastavitev potisnih obvestil</string> - <string name="push_server_install_app_description"> Za prejemanje potisnih obvestil namestite aplikacijo, ki podpira [Unified Push](https://unifiedpush.org/), na primer [Ntfy](https://ntfy.sh/). - Po namestitvi izberite aplikacijo, ki jo želite uporabljati, v nastavitvah. - </string> + <string name="push_server_install_app_description">Za prejemanje potisnih obvestil namestite aplikacijo, ki podpira [Unified Push](https://unifiedpush.org/), na primer [Ntfy](https://ntfy.sh/). Po namestitvi izberite aplikacijo, ki jo želite uporabljati, v nastavitvah.</string> <string name="thread_title">Niz objav</string> <string name="send_the_seller_a_message">Pošlji prodajalcu sporočilo</string> <string name="hi_there_is_this_still_available">Živjo, je to še na voljo?</string> @@ -1856,7 +1846,7 @@ <string name="calendar_day_a11y_today_suffix">danes</string> <string name="calendar_day_a11y_selected_suffix">izbrano</string> <string name="calendar_fab_new_event">Ustvari nov koledarski dogodek</string> - <string name="calendar_fab_new_collection">Ustvari novo zbirko koledarjev </string> + <string name="calendar_fab_new_collection">Ustvari novo zbirko koledarjev</string> <string name="calendar_fab_toggle">Ustvari novo zbirko koledarjev</string> <string name="calendar_untitled">(neimenovano)</string> <string name="calendar_all_day">Ves dan</string> @@ -2042,7 +2032,7 @@ <string name="buzz_dm_more">Več</string> <string name="buzz_dm_add_member">Dodaj člana</string> <string name="buzz_dm_add_member_title">Dodaj nekoga v to ZS</string> - <string name="buzz_dm_add_member_invalid">Neveljaven npub ali hex ključ </string> + <string name="buzz_dm_add_member_invalid">Neveljaven npub ali hex ključ</string> <string name="buzz_dm_just_you">Samo vi</string> <string name="buzz_dm_workspace">Delovni prostor</string> <string name="buzz_dm_recipients">Za</string> @@ -2091,8 +2081,7 @@ <string name="dm_relays_not_found">Nastavite releje za predal zasebnih sporočil</string> <string name="dm_relays_not_found_description">Ta nastavitev omogoča vsem, da vedo, katere releje uporabiti, ko vam pošiljajo sporočila. Brez njih bi lahko zgrešili nekatera sporočila.</string> <string name="dm_relays_not_found_examples2">Dobre možnosti so:\n - auth.nostr1.com (brezplačen)\n - inbox.nostr.wine (plačljiv)\n - relay.0xchat.com (brezplačen)</string> - <string name="dm_relays_not_found_editing">Vnesite 1–3 releje, ki bodo služili kot vaš predal - za zasebna sporočila. Releji za zasebna sporočila bi morali sprejeti katero koli sporočilo od kogar koli, vendar samo vam dovoliti njihov prenos.</string> + <string name="dm_relays_not_found_editing">Vnesite 1–3 releje, ki bodo služili kot vaš predal za zasebna sporočila. Releji za zasebna sporočila bi morali sprejeti katero koli sporočilo od kogar koli, vendar samo vam dovoliti njihov prenos.</string> <string name="dm_relays_not_found_create_now">Nastavi zdaj</string> <string name="recipient_missing_dm_relays">Releji za prejeto pošto (ZS) niso bili najdeni. Sporočil ni mogoče dostaviti, dokler uporabnik ne nastavi seznama svojih relejev.</string> <string name="search_relays_not_found">Nastavi iskalne releje</string> @@ -2339,7 +2328,7 @@ <string name="new_chess_game">Nova partija šaha</string> <string name="chess_loading_game">Nalagam partijo\u2026</string> <string name="chess_game_not_found">Partija ni najdena</string> - <string name="chess_game_waiting">Ta partija je zaključena ali pa, čaka na nasprotnika </string> + <string name="chess_game_waiting">Ta partija je zaključena ali pa, čaka na nasprotnika</string> <string name="chess_game_id">ID Partije: %1$s\u2026</string> <string name="badge_and_n_others"> in %1$d ostalih</string> <string name="remove_language">Odstrani %1$s</string> @@ -2664,9 +2653,7 @@ committed the change, %2$s the member it concerns, %3$s the new name. --> <string name="marmot_remove_photo">Odstrani fotografijo</string> <string name="marmot_keypackage_relays_not_set_title">KeyPackage releji niso nastavljeni</string> - <string name="marmot_keypackage_relays_not_set_message">Še nimate seznama relejev za KeyPackage (MIP-00). Ta seznam drugim sporoča, kje je objavljen vaš KeyPackage, da vas lahko povabijo v skupinske klepete. - -Ali želite za to uporabiti trenutne releje iz odhodnega predala?</string> + <string name="marmot_keypackage_relays_not_set_message">Še nimate seznama relejev za KeyPackage (MIP-00). Ta seznam drugim sporoča, kje je objavljen vaš KeyPackage, da vas lahko povabijo v skupinske klepete. Ali želite za to uporabiti trenutne releje iz odhodnega predala?</string> <string name="marmot_use_outbox_relays">Uporabi odhodne releje</string> <string name="marmot_skip_for_now">Zaenkrat preskoči</string> <string name="marmot_group_info_title">Podatki o skupini</string> @@ -3337,15 +3324,7 @@ Ali želite za to uporabiti trenutne releje iz odhodnega predala?</string> <string name="could_not_fetch_invoice_from_details">Ni bilo mogoče pridobiti fakture od %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Stisnjene datoteke ni bilo mogoče odpreti</string> <string name="couldnt_find_nwc_wallets">Ne najdem denarnice</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst ni našel nobene denarnice, ki podpira Nostr Wallet Connect (NWC). -\n\nKaj lahko storiš: -\n\t1. Preverite podporo za NWC: Ponovno\t preverite, ali vaša priljubljena denarnica podpira Nostr Wallet Connect. -\n\t2. Ustvari povezavo: Odprite svojo denarnico in ustvarite novo NWC povezavo. -\n\t3. Povežite z Amethyst: Vaša denarnica vam bo ponavadi ponudila tri možnosti: -\n\t\t3.1 Samodejna povezava: Gumb za samodejno odprtje in povezavo z Amethyst. Če je na voljo, kliknite ta gumb, odprite Amethyst in na novem zaslonu pritisnite shrani. -\n\t\tRočna povezava: Povezovalni URI, ki ga lahko kopirate, nato pa na tem zaslonu kliknite ikono za lepljenje, da ga neposredno vnesete. -\n\t\tQR koda: Denarnica ustvari QR-kodo, ki jo skenirajte z gumbom za QR na tem zaslonu. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst ni našel nobene denarnice, ki podpira Nostr Wallet Connect (NWC). \n\nKaj lahko storiš: \n\t1. Preverite podporo za NWC: Ponovno\t preverite, ali vaša priljubljena denarnica podpira Nostr Wallet Connect. \n\t2. Ustvari povezavo: Odprite svojo denarnico in ustvarite novo NWC povezavo. \n\t3. Povežite z Amethyst: Vaša denarnica vam bo ponavadi ponudila tri možnosti: \n\t\t3.1 Samodejna povezava: Gumb za samodejno odprtje in povezavo z Amethyst. Če je na voljo, kliknite ta gumb, odprite Amethyst in na novem zaslonu pritisnite shrani. \n\t\tRočna povezava: Povezovalni URI, ki ga lahko kopirate, nato pa na tem zaslonu kliknite ikono za lepljenje, da ga neposredno vnesete. \n\t\tQR koda: Denarnica ustvari QR-kodo, ki jo skenirajte z gumbom za QR na tem zaslonu.</string> <string name="create">Ustvari</string> <string name="d">d</string> <string name="danger_zone">Nevarno območje</string> @@ -3804,26 +3783,16 @@ Ali želite za to uporabiti trenutne releje iz odhodnega predala?</string> <string name="location_change_place">Spremeni lokacijo na karti</string> <string name="location_channels">Lokacijski kanali</string> <string name="location_pick_on_map">Izberi lokacijo na karti</string> - <string name="login_with_a_private_key_to_be_able_to_boost_posts">Uporabljaš javni ključ in javni ključi omogočajo le branje. -Prijavi se s privatnim ključem za posredovanje sporočila</string> - <string name="login_with_a_private_key_to_be_able_to_follow">Uporabljaš javni ključ in javni ključi omogočajo le branje. -Prijavi se s privatnim ključem, za sledenje uporabnika</string> - <string name="login_with_a_private_key_to_be_able_to_hide_word">Uporabljaš javni ključ in javni ključi omogočajo le branje. -Prijavi se s privatnim ključem, za cenzuro besed ali stavkov</string> - <string name="login_with_a_private_key_to_be_able_to_reply">Uporabljaš javni ključ in javni ključi omogočajo le branje. -Prijavi se s privatnim ključem, da omogočiš tudi pisanje</string> - <string name="login_with_a_private_key_to_be_able_to_send_zaps">Uporabljaš javni ključ in javni ključi omogočajo le branje. -Prijavite se s privatnim ključem, da boste lahko pošiljali Zape</string> - <string name="login_with_a_private_key_to_be_able_to_show_word">Uporabljaš javni ključ in javni ključi omogočajo le branje. -Prijavi se s privatnim ključem, za prikaz skritih besed in stavkov</string> - <string name="login_with_a_private_key_to_be_able_to_sign_events">Uporabljaš javni ključ in javni ključi omogočajo le branje. -Za podpisovanje se je potrebno prijaviti s privatnim ključem</string> - <string name="login_with_a_private_key_to_be_able_to_unfollow">Uporabljaš javni ključ in javni ključi omogočajo le branje. -Prijavi se s privatnim ključem, za odstranitev iz seznama sledljivih</string> - <string name="login_with_a_private_key_to_be_able_to_upload">Uporabljaš javni ključ in javni ključi omogočajo le branje. -Za nalaganje se je potrebno prijaviti s privatnim ključem</string> - <string name="login_with_a_private_key_to_like_posts">Uporabljaš javni ključ in javni ključi omogočajo le branje. -Prijavi se s privatnim ključem za všečkanje sporočila</string> + <string name="login_with_a_private_key_to_be_able_to_boost_posts">Uporabljaš javni ključ in javni ključi omogočajo le branje. Prijavi se s privatnim ključem za posredovanje sporočila</string> + <string name="login_with_a_private_key_to_be_able_to_follow">Uporabljaš javni ključ in javni ključi omogočajo le branje. Prijavi se s privatnim ključem, za sledenje uporabnika</string> + <string name="login_with_a_private_key_to_be_able_to_hide_word">Uporabljaš javni ključ in javni ključi omogočajo le branje. Prijavi se s privatnim ključem, za cenzuro besed ali stavkov</string> + <string name="login_with_a_private_key_to_be_able_to_reply">Uporabljaš javni ključ in javni ključi omogočajo le branje. Prijavi se s privatnim ključem, da omogočiš tudi pisanje</string> + <string name="login_with_a_private_key_to_be_able_to_send_zaps">Uporabljaš javni ključ in javni ključi omogočajo le branje. Prijavite se s privatnim ključem, da boste lahko pošiljali Zape</string> + <string name="login_with_a_private_key_to_be_able_to_show_word">Uporabljaš javni ključ in javni ključi omogočajo le branje. Prijavi se s privatnim ključem, za prikaz skritih besed in stavkov</string> + <string name="login_with_a_private_key_to_be_able_to_sign_events">Uporabljaš javni ključ in javni ključi omogočajo le branje. Za podpisovanje se je potrebno prijaviti s privatnim ključem</string> + <string name="login_with_a_private_key_to_be_able_to_unfollow">Uporabljaš javni ključ in javni ključi omogočajo le branje. Prijavi se s privatnim ključem, za odstranitev iz seznama sledljivih</string> + <string name="login_with_a_private_key_to_be_able_to_upload">Uporabljaš javni ključ in javni ključi omogočajo le branje. Za nalaganje se je potrebno prijaviti s privatnim ključem</string> + <string name="login_with_a_private_key_to_like_posts">Uporabljaš javni ključ in javni ključi omogočajo le branje. Prijavi se s privatnim ključem za všečkanje sporočila</string> <string name="longs">Videoposnetki</string> <string name="m">m</string> <string name="manage_emoji_packs">Moji emoji paketi</string> @@ -4122,7 +4091,7 @@ Prijavi se s privatnim ključem za všečkanje sporočila</string> <string name="new_conversation_public_chat_con_2">Brez moderacije</string> <string name="new_conversation_public_chat_cta">Ustvari nov javen klepet</string> <string name="new_conversation_public_chat_pro_1">Pridruži se lahko kdorkoli</string> - <string name="new_conversation_public_chat_pro_2">Preprosto in zlahka dosegljivo </string> + <string name="new_conversation_public_chat_pro_2">Preprosto in zlahka dosegljivo</string> <string name="new_conversation_public_chat_tagline">Javna sporočila na releju</string> <string name="new_conversation_public_chat_title">Javni klepet</string> <string name="new_conversation_relay_group_best">Skupine, ki jih moderira njihov gostiteljski rele</string> @@ -4531,11 +4500,8 @@ Prijavi se s privatnim ključem za všečkanje sporočila</string> <string name="proxy_section">Proxy releji</string> <string name="proxy_section_explainer">Agregatorji, ki jih mora aplikacija uporabljati za prenos vaših vsebin, na primer filter.nostr.wine. To nadomesti model odhodne pošte (outbox model) in bo prisililo aplikacijo k povezovanju prek relejev iz vašega seznama.</string> <string name="public_chat">Javni pogovor</string> - <string name="public_chat_explainer">Javni klepeti so vidni vsem na Nostru in vsakdo - se lahko pridruži. So odlični za odprte skupnosti o specifičnih temah. - Moderiranje je možno z brisanjem objav na njihovih relejih</string> - <string name="public_chat_relays_explainer">Vnesi 1–3 releje, ki gostijo to skupino. - Nostr odjemalci uporabljajo to nastavitev, da vedo, od kod prenesti vaša sporočila in kam jih poslati.</string> + <string name="public_chat_explainer">Javni klepeti so vidni vsem na Nostru in vsakdo se lahko pridruži. So odlični za odprte skupnosti o specifičnih temah. Moderiranje je možno z brisanjem objav na njihovih relejih</string> + <string name="public_chat_relays_explainer">Vnesi 1–3 releje, ki gostijo to skupino. Nostr odjemalci uporabljajo to nastavitev, da vedo, od kod prenesti vaša sporočila in kam jih poslati.</string> <string name="public_chat_relays_title">Releji</string> <string name="public_chat_title">Metapodatki Javnega Klepeta</string> <string name="public_chats">Javni klepeti</string> @@ -4595,10 +4561,7 @@ Prijavi se s privatnim ključem za všečkanje sporočila</string> <string name="relay_auth_search_keywords">Preverjanje pristnosti (Auth), rele (Relay), podpis (Sign), preverjanje (Verify), NIP-42, identiteta</string> <string name="relay_auth_settings_title">Avtentikacija releja</string> <string name="relay_chat">Rele klepet</string> - <string name="relay_chat_explainer">Pogovori prek relejev so pogovorne skupine, ki jih nadzoruje njihov domači rele. - Vidne so vsem na Nostru in v njih lahko sodeluje kdorkoli. - So odlične za odprte skupnosti, povezane s specifičnimi temami. Nekatere od teh skupin so kratkotrajne - zato sporočila v klepetu sčasoma izginejo</string> + <string name="relay_chat_explainer">Pogovori prek relejev so pogovorne skupine, ki jih nadzoruje njihov domači rele. Vidne so vsem na Nostru in v njih lahko sodeluje kdorkoli. So odlične za odprte skupnosti, povezane s specifičnimi temami. Nekatere od teh skupin so kratkotrajne zato sporočila v klepetu sčasoma izginejo</string> <string name="relay_chat_title">Rele klepeti</string> <string name="relay_explain_account_data">Vaš profil, nastavitve in osnutki na vaših domačih relejih.</string> <string name="relay_explain_community_chats">Releji, kamor vsaka skupnost objavlja svoje načrte.</string> @@ -4732,7 +4695,7 @@ Prijavi se s privatnim ključem za všečkanje sporočila</string> <string name="resource_usage_memory_native">Lokalni pomnilnik</string> <string name="resource_usage_memory_notes">Zapiski v pomnilniku</string> <string name="resource_usage_memory_section">Trenutni pomnilnik</string> - <string name="resource_usage_memory_users">Profili v pomnilniku </string> + <string name="resource_usage_memory_users">Profili v pomnilniku</string> <string name="resource_usage_nests">Čas v zvočnih sobah</string> <string name="resource_usage_pow">Rudarjenje PoW</string> <string name="resource_usage_radio_bursts">Obuditve radijskega modula za prenose (ocena)</string> diff --git a/commonsUI/src/commonMain/composeResources/values-sr-rSP/strings.xml b/commonsUI/src/commonMain/composeResources/values-sr-rSP/strings.xml index 6ea50394e6..9aee9b1e4d 100644 --- a/commonsUI/src/commonMain/composeResources/values-sr-rSP/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-sr-rSP/strings.xml @@ -271,15 +271,8 @@ <string name="mark_all_known_as_read">Označi sve poznate kao pročitano</string> <string name="mark_all_new_as_read">Označi sve novo kao pročitano</string> <string name="mark_all_as_read">Označi sve kao pročitano</string> - <string name="account_backup_tips2_md"> ## Saveti za bekap ključa i bezbednost - \n\nVaš nalog je zaštićen tajnim ključem. Ključ je dugačak niz znakova koji počinje sa **nsec1**. Svako ko ima pristup ovom tajnom ključu može da objavljuje i menja vaš identitet. - \n\n- **Ne** unosite vaš tajni ključ ni na jedan veb-sajt ili softver kojemu ne verujete. - \n- Amethyst programeri **nikada** neće tražiti vaš tajni ključ. - \n- **Čuvajte** bezbednu kopiju vašeg tajnog ključa radi oporavka naloga. Preporučujemo korišćenje menadžera lozinki. - </string> - <string name="account_backup_tips3_md"> Za dodatnu bezbednost, možete šifrovati vaš ključ lozinkom. Ovaj ključ počinje sa **ncryptsec1** i ne može se koristiti bez vaše lozinke. - \n\nAko izgubite lozinku, nećete moći da obnovite ključ. - </string> + <string name="account_backup_tips2_md">## Saveti za bekap ključa i bezbednost \n\nVaš nalog je zaštićen tajnim ključem. Ključ je dugačak niz znakova koji počinje sa **nsec1**. Svako ko ima pristup ovom tajnom ključu može da objavljuje i menja vaš identitet. \n\n- **Ne** unosite vaš tajni ključ ni na jedan veb-sajt ili softver kojemu ne verujete. \n- Amethyst programeri **nikada** neće tražiti vaš tajni ključ. \n- **Čuvajte** bezbednu kopiju vašeg tajnog ključa radi oporavka naloga. Preporučujemo korišćenje menadžera lozinki.</string> + <string name="account_backup_tips3_md">Za dodatnu bezbednost, možete šifrovati vaš ključ lozinkom. Ovaj ključ počinje sa **ncryptsec1** i ne može se koristiti bez vaše lozinke. \n\nAko izgubite lozinku, nećete moći da obnovite ključ.</string> <string name="encrypt_and_copy_my_secret_key">Šifruj i kopiraj moj tajni ključ</string> <string name="badge_award_image_for">Slika značke za %1$s</string> <string name="badge_award_image">"Slika značke</string> @@ -959,9 +952,7 @@ <string name="push_server_none_explainer">Onemogućava push obaveštenja</string> <string name="push_server_uses_app_explainer">Koristi aplikaciju %1$s</string> <string name="push_server_install_app">Podešavanje push obaveštenja</string> - <string name="push_server_install_app_description"> Da biste primali push obaveštenja, instalirajte bilo koju aplikaciju koja podržava [Unified Push](https://unifiedpush.org/), kao što je [Ntfy](https://ntfy.sh/). - Nakon instalacije, izaberite aplikaciju koju želite da koristite u Postavkama. - </string> + <string name="push_server_install_app_description">Da biste primali push obaveštenja, instalirajte bilo koju aplikaciju koja podržava [Unified Push](https://unifiedpush.org/), kao što je [Ntfy](https://ntfy.sh/). Nakon instalacije, izaberite aplikaciju koju želite da koristite u Postavkama.</string> <string name="thread_title">Nit</string> <string name="send_the_seller_a_message">Pošaljite prodavcu poruku</string> <string name="hi_there_is_this_still_available">Zdravo, da li je ovo još uvek dostupno?</string> @@ -2014,15 +2005,7 @@ <string name="could_not_fetch_invoice_from_details">Nije moguće dohvatiti fakturu od %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Komprimovana datoteka se ne može otvoriti</string> <string name="couldnt_find_nwc_wallets">Novčanik nije pronađen</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst nije mogao da pronađe nijednu aplikaciju novčanika koja podržava Nostr Wallet Connect (NWC). -\n\nŠta možete da uradite: -\n\t1. Proverite NWC podršku: Proverite da li vaša preferovana aplikacija novčanika podržava Nostr Wallet Connect. -\n\t2. Kreirajte vezu: Idite na aplikaciju novčanika i kreirajte novu NWC vezu. -\n\t3. Povežite sa Amethyst: Vaš novčanik će obično ponuditi tri opcije: -\n\t\t3.1 Automatska veza: Dugme za automatsko otvaranje i povezivanje sa Amethyst. Ako postoji, kliknite to dugme, otvorite Amethyst i pritisnite sačuvaj na novom ekranu. -\n\t\t3.2 Ručna veza: URI veze koji možete kopirati i kliknuti ikonu za nalepljivanje na ovom ekranu da ga direktno unesete. -\n\t\t3.3 QR kod: Novčanik kreira QR kod koji možete očitati dugmetom za QR kod na ovom ekranu. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst nije mogao da pronađe nijednu aplikaciju novčanika koja podržava Nostr Wallet Connect (NWC). \n\nŠta možete da uradite: \n\t1. Proverite NWC podršku: Proverite da li vaša preferovana aplikacija novčanika podržava Nostr Wallet Connect. \n\t2. Kreirajte vezu: Idite na aplikaciju novčanika i kreirajte novu NWC vezu. \n\t3. Povežite sa Amethyst: Vaš novčanik će obično ponuditi tri opcije: \n\t\t3.1 Automatska veza: Dugme za automatsko otvaranje i povezivanje sa Amethyst. Ako postoji, kliknite to dugme, otvorite Amethyst i pritisnite sačuvaj na novom ekranu. \n\t\t3.2 Ručna veza: URI veze koji možete kopirati i kliknuti ikonu za nalepljivanje na ovom ekranu da ga direktno unesete. \n\t\t3.3 QR kod: Novčanik kreira QR kod koji možete očitati dugmetom za QR kod na ovom ekranu.</string> <string name="create">Napravi</string> <string name="danger_zone">Opasna zona</string> <string name="dark">Tamno</string> @@ -2703,11 +2686,8 @@ <string name="proxy_section">Proxy Relay-i</string> <string name="proxy_section_explainer">Agregatorni relay-i koje aplikacija mora koristiti za preuzimanje vaših feedova, poput filter.nostr.wine. Ovo zamenjuje outbox model i čini da se aplikacija povezuje samo na relay-e u vašim listama.</string> <string name="public_chat">Javni čet</string> - <string name="public_chat_explainer">Javni četovi vidljivi su svima na Nostr-u i svako - može da učestvuje. Odlični su za otvorene zajednice oko specifičnih tema. - Moderacija se može kontrolisati brisanjem objava na relay-ima</string> - <string name="public_chat_relays_explainer">Unesite između 1 i 3 relay-a koji hostuju ovu grupu. - Nostr klijenti koriste ovo podešavanje da znaju odakle da preuzmu poruke i kuda da ih šalju.</string> + <string name="public_chat_explainer">Javni četovi vidljivi su svima na Nostr-u i svako može da učestvuje. Odlični su za otvorene zajednice oko specifičnih tema. Moderacija se može kontrolisati brisanjem objava na relay-ima</string> + <string name="public_chat_relays_explainer">Unesite između 1 i 3 relay-a koji hostuju ovu grupu. Nostr klijenti koriste ovo podešavanje da znaju odakle da preuzmu poruke i kuda da ih šalju.</string> <string name="public_chat_relays_title">Relay-i</string> <string name="public_chat_title">Metapodaci javnog četa</string> <string name="public_chats">Javni razgovori</string> @@ -2732,10 +2712,7 @@ <string name="read_only_user">Korisnik samo za čitanje</string> <string name="record_a_message_description">Kliknite i držite za snimanje poruke</string> <string name="relay_chat">Relay čet</string> - <string name="relay_chat_explainer">Relay četovi su grupe za razgovor kojima upravlja njihov matični relay. - Vidljivi su svima na Nostr-u i svako može da učestvuje. - Odlični su za otvorene zajednice oko specifičnih tema. Neke od ovih grupa su efemeralne - i stoga poruke nestaju s vremenom</string> + <string name="relay_chat_explainer">Relay četovi su grupe za razgovor kojima upravlja njihov matični relay. Vidljivi su svima na Nostr-u i svako može da učestvuje. Odlični su za otvorene zajednice oko specifičnih tema. Neke od ovih grupa su efemeralne i stoga poruke nestaju s vremenom</string> <string name="relay_chat_title">Relay četovi</string> <string name="relay_management_tab_events">Događaji</string> <string name="relay_management_tab_ips">IP adrese</string> diff --git a/commonsUI/src/commonMain/composeResources/values-sv-rSE/strings.xml b/commonsUI/src/commonMain/composeResources/values-sv-rSE/strings.xml index f591cc0177..0b434776f0 100644 --- a/commonsUI/src/commonMain/composeResources/values-sv-rSE/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-sv-rSE/strings.xml @@ -361,7 +361,7 @@ <string name="copy_user_id">Kopiera Användarens ID</string> <string name="unblock_user">Sluta blockera Användare</string> <string name="user_is_blocked_hidden">Den här användaren är blockerad/tystad. Deras inlägg är dolda.</string> - <string name="npub_hex_username">npub, hex, användarnamn </string> + <string name="npub_hex_username">npub, hex, användarnamn</string> <string name="clear">Rensa</string> <string name="app_logo">Appens logo</string> <string name="nsec_npub_hex_private_key">nsec / npub / hex privat nyckel</string> @@ -463,13 +463,8 @@ <string name="mark_all_known_as_read">Markera alla kända som lästa</string> <string name="mark_all_new_as_read">Markera alla nya som lästa</string> <string name="mark_all_as_read">Markera allt som läst</string> - <string name="account_backup_tips2_md"> ## Nyckelsäkerhetsråd - \n\nDitt konto är säkrat med en hemlig nyckel. Nyckeln är en lång följd av tecken som börjar med **nsec1**. Den som har tillgång till denna hemliga nyckel kan posta och ändra din identitet. - \n\n- **Lägg inte** din hemliga nyckel på någon webbplats eller i någon mjukvara som du inte litar på. - \n- Utvecklarna av Amethyst kommer **aldrig** att be om din hemliga nyckel. - \n- **Spara** en säkerhetskopia av din hemliga nyckel för återställning av kontot. Vi rekommenderar att du använder en lösenordshanterare.</string> - <string name="account_backup_tips3_md"> För extra säkerhet kan du kryptera din nyckel med ett lösenord. Denna nyckel börjar med **ncryptsec1** och kan inte användas utan ditt lösenord. - \n\nOm du tappar bort ditt lösenord kommer du inte att kunna återställa din nyckel.</string> + <string name="account_backup_tips2_md">## Nyckelsäkerhetsråd \n\nDitt konto är säkrat med en hemlig nyckel. Nyckeln är en lång följd av tecken som börjar med **nsec1**. Den som har tillgång till denna hemliga nyckel kan posta och ändra din identitet. \n\n- **Lägg inte** din hemliga nyckel på någon webbplats eller i någon mjukvara som du inte litar på. \n- Utvecklarna av Amethyst kommer **aldrig** att be om din hemliga nyckel. \n- **Spara** en säkerhetskopia av din hemliga nyckel för återställning av kontot. Vi rekommenderar att du använder en lösenordshanterare.</string> + <string name="account_backup_tips3_md">För extra säkerhet kan du kryptera din nyckel med ett lösenord. Denna nyckel börjar med **ncryptsec1** och kan inte användas utan ditt lösenord. \n\nOm du tappar bort ditt lösenord kommer du inte att kunna återställa din nyckel.</string> <string name="backup_keys_nudge_title">Säkerhetskopiera dina nycklar</string> <string name="backup_keys_nudge_body">Din hemliga nyckel är enda sättet att komma åt det här kontot. Om du förlorar den kan den aldrig återskapas. Spara den på ett säkert ställe nu.</string> <string name="backup_keys_nudge_backup_now">Säkerhetskopiera nu</string> @@ -478,7 +473,7 @@ <string name="encrypt_and_copy_my_secret_key">Kryptera och kopiera min hemliga nyckel</string> <string name="badge_award_image_for">Tilldelad Badge för %1$s</string> <string name="badge_award_image">Bild på utmärkelse</string> - <string name="new_badge_award_notif">Du har blivit tilldelad en ny Badge </string> + <string name="new_badge_award_notif">Du har blivit tilldelad en ny Badge</string> <string name="award_granted_to">Badge utmärkelse tilldelas</string> <string name="account_switch_add_account_dialog_title">Lägg till nytt konto</string> <string name="account_switch_select_account">Välj konto</string> @@ -1584,9 +1579,7 @@ <string name="push_server_none_explainer">Inaktivera Push-meddelande</string> <string name="push_server_uses_app_explainer">Använder appen %1$s</string> <string name="push_server_install_app">Inställning av Push-aviseringar</string> - <string name="push_server_install_app_description"> För att ta emot push-aviseringar, installera vilken app som helst som stöder [Unified Push](https://unifiedpush.org), till exempel [Ntfy](https://ntfy.sh/). - Efter installationen väljer du den app du vill använda i inställningarna. - </string> + <string name="push_server_install_app_description">För att ta emot push-aviseringar, installera vilken app som helst som stöder [Unified Push](https://unifiedpush.org), till exempel [Ntfy](https://ntfy.sh/). Efter installationen väljer du den app du vill använda i inställningarna.</string> <string name="thread_title">Tråd</string> <string name="send_the_seller_a_message">Skicka ett meddelande till säljaren</string> <string name="hi_there_is_this_still_available">Hallå där, är detta fortfarande tillgängligt?</string> @@ -3323,14 +3316,7 @@ <string name="could_not_fetch_invoice_from_details">Kunde inte hämta faktura från %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Kunde inte öppna den komprimerade filen</string> <string name="couldnt_find_nwc_wallets">Plånbok hittades inte</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst kunde inte hitta några plånboksappar som stöder Nostr Wallet Connect (NWC). -\n\nVad du kan göra: -\n\t1. Verifiera NWC-stöd: Kontrollera att din föredragna plånbok stöder Nostr Wallet Connect. -\n\t2. Skapa anslutning: Gå till din plånbok och skapa en ny NWC-anslutning. -\n\t3. Länka till Amethyst: Din plånbok erbjuder vanligtvis tre alternativ: -\n\t\t3.1 Automatisk länk: En knapp för att automatiskt öppna och ansluta till Amethyst. Klicka på den, öppna Amethyst och spara på den nya skärmen. -\n\t\t3.2 Manuell länk: En anslutnings-URI som du kan kopiera och klistra in via klistra-ikonen på den här skärmen. -\n\t\t3.3 QR-kod: Plånboken skapar en QR-kod som du kan skanna med QR-knappens funktion på den här skärmen.</string> + <string name="couldnt_find_nwc_wallets_description">Amethyst kunde inte hitta några plånboksappar som stöder Nostr Wallet Connect (NWC). \n\nVad du kan göra: \n\t1. Verifiera NWC-stöd: Kontrollera att din föredragna plånbok stöder Nostr Wallet Connect. \n\t2. Skapa anslutning: Gå till din plånbok och skapa en ny NWC-anslutning. \n\t3. Länka till Amethyst: Din plånbok erbjuder vanligtvis tre alternativ: \n\t\t3.1 Automatisk länk: En knapp för att automatiskt öppna och ansluta till Amethyst. Klicka på den, öppna Amethyst och spara på den nya skärmen. \n\t\t3.2 Manuell länk: En anslutnings-URI som du kan kopiera och klistra in via klistra-ikonen på den här skärmen. \n\t\t3.3 QR-kod: Plånboken skapar en QR-kod som du kan skanna med QR-knappens funktion på den här skärmen.</string> <string name="create">Skapa</string> <string name="d">d</string> <string name="danger_zone">Riskzon</string> @@ -4427,11 +4413,8 @@ <string name="proxy_section">Proxyreläer</string> <string name="proxy_section_explainer">Aggregeringsreläer som används för att hämta flöden, t.ex. filter.nostr.wine</string> <string name="public_chat">Publik Chat</string> - <string name="public_chat_explainer">Offentliga chattar är synliga för alla på Nostr och alla - kan delta på dem. De är bra för öppna samhällen kring specifika ämnen. - Moderation kan kontrolleras genom att ta bort inlägg på reläerna</string> - <string name="public_chat_relays_explainer">Infoga mellan 1–3 reläer som är värd för denna grupp. - Nostr klienter använder denna inställning för att veta var du kan ladda ner meddelanden från och skicka dina meddelanden till.</string> + <string name="public_chat_explainer">Offentliga chattar är synliga för alla på Nostr och alla kan delta på dem. De är bra för öppna samhällen kring specifika ämnen. Moderation kan kontrolleras genom att ta bort inlägg på reläerna</string> + <string name="public_chat_relays_explainer">Infoga mellan 1–3 reläer som är värd för denna grupp. Nostr klienter använder denna inställning för att veta var du kan ladda ner meddelanden från och skicka dina meddelanden till.</string> <string name="public_chat_relays_title">Reläer</string> <string name="public_chat_title">Metadata för offentlig chatt</string> <string name="public_chats">Offentliga chattar</string> @@ -4496,10 +4479,7 @@ <string name="relay_auth_search_keywords">auth autentisering relä signera verifiera nip-42 identitet</string> <string name="relay_auth_settings_title">Reläautentisering</string> <string name="relay_chat">Relä Chatt</string> - <string name="relay_chat_explainer">Relä chattar är chattgrupper som kontrolleras av deras hem relä. - De är synliga för alla på Nostr och vem som helst kan delta på dem. - De är bra för öppna gemenskaper kring specifika ämnen. Några av dessa grupper är kortlivade - och därmed försvinner chattmeddelanden över tiden</string> + <string name="relay_chat_explainer">Relä chattar är chattgrupper som kontrolleras av deras hem relä. De är synliga för alla på Nostr och vem som helst kan delta på dem. De är bra för öppna gemenskaper kring specifika ämnen. Några av dessa grupper är kortlivade och därmed försvinner chattmeddelanden över tiden</string> <string name="relay_chat_title">Relä Chattar</string> <string name="relay_explain_account_data">Din egen profil, dina inställningar och utkast, på dina hemreläer.</string> <string name="relay_explain_community_chats">Reläerna som varje gemenskap publicerar sina plan till.</string> diff --git a/commonsUI/src/commonMain/composeResources/values-sw/strings.xml b/commonsUI/src/commonMain/composeResources/values-sw/strings.xml index 9c24ec5784..439b9c7be0 100644 --- a/commonsUI/src/commonMain/composeResources/values-sw/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-sw/strings.xml @@ -268,15 +268,8 @@ <string name="mark_all_known_as_read">Funga kama Zimejulikana</string> <string name="mark_all_new_as_read">Funga kama Mpya</string> <string name="mark_all_as_read">Funga kama Zimejulikana</string> - <string name="account_backup_tips2_md"> ## Vidokezo vya Nakala Rudufu ya Ufunguo na Usalama - \n\nAkaunti yako inalindwa na ufunguo wa siri. Ufunguo ni mfululizo mrefu wa herufi unaoanza na **nsec1**. Mtu yeyote anayepata ufunguo huu wa siri anaweza kuchapisha na kubadilisha utambulisho wako. - \n\n- **Usiweke** ufunguo wako wa siri katika tovuti yoyote au programu unayoitembelewa. - \n- Watengenezaji wa Amethyst **hawatawahi** kuomba ufunguo wako wa siri. - \n- **Hifadhi** nakala salama ya ufunguo wako wa siri kwa ajili ya kurejesha akaunti. Tunapendekeza kutumia kidhibiti cha nenosiri. - </string> - <string name="account_backup_tips3_md"> Kwa usalama zaidi, unaweza kusimba ufunguo wako kwa nenosiri. Ufunguo huu unaanza na **ncryptsec1** na hauwezi kutumika bila nenosiri lako. - \n\nUkipoteza nenosiri lako, hutaweza kurejesha ufunguo wako. - </string> + <string name="account_backup_tips2_md">## Vidokezo vya Nakala Rudufu ya Ufunguo na Usalama \n\nAkaunti yako inalindwa na ufunguo wa siri. Ufunguo ni mfululizo mrefu wa herufi unaoanza na **nsec1**. Mtu yeyote anayepata ufunguo huu wa siri anaweza kuchapisha na kubadilisha utambulisho wako. \n\n- **Usiweke** ufunguo wako wa siri katika tovuti yoyote au programu unayoitembelewa. \n- Watengenezaji wa Amethyst **hawatawahi** kuomba ufunguo wako wa siri. \n- **Hifadhi** nakala salama ya ufunguo wako wa siri kwa ajili ya kurejesha akaunti. Tunapendekeza kutumia kidhibiti cha nenosiri.</string> + <string name="account_backup_tips3_md">Kwa usalama zaidi, unaweza kusimba ufunguo wako kwa nenosiri. Ufunguo huu unaanza na **ncryptsec1** na hauwezi kutumika bila nenosiri lako. \n\nUkipoteza nenosiri lako, hutaweza kurejesha ufunguo wako.</string> <string name="encrypt_and_copy_my_secret_key">Simba na nakili ufunguo wangu wa siri</string> <string name="badge_award_image_for">Picha ya tuzo ya alama kwa %1$s</string> <string name="badge_award_image">"Picha ya tuzo ya beji</string> @@ -961,9 +954,7 @@ <string name="push_server_none_explainer">Inazima Arifa za Push</string> <string name="push_server_uses_app_explainer">Inatumia programu %1$s</string> <string name="push_server_install_app">Mpangilio wa Arifa za Push</string> - <string name="push_server_install_app_description"> Ili kupokea arifa za kusukuma, sakinisha programu yoyote inayounga mkono [Unified Push](https://unifiedpush.org/), kama vile [Ntfy](https://ntfy.sh/). - Baada ya kusakinisha, chagua programu unayotaka kutumia katika Mipangilio. - </string> + <string name="push_server_install_app_description">Ili kupokea arifa za kusukuma, sakinisha programu yoyote inayounga mkono [Unified Push](https://unifiedpush.org/), kama vile [Ntfy](https://ntfy.sh/). Baada ya kusakinisha, chagua programu unayotaka kutumia katika Mipangilio.</string> <string name="thread_title">Nyuzi</string> <string name="send_the_seller_a_message">Tuma ujumbe kwa muuzaji</string> <string name="hi_there_is_this_still_available">Habari, je, hii bado inapatikana?</string> @@ -2016,15 +2007,7 @@ <string name="could_not_fetch_invoice_from_details">Haikuweza kupata ankara kutoka kwa %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Haikuweza kufungua faili lililosinyazwa</string> <string name="couldnt_find_nwc_wallets">Mkoba haukupatikana</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst haikupata programu yoyote ya mkoba inayounga mkono Nostr Wallet Connect (NWC). -\n\nUnachoweza kufanya: -\n\t1. Thibitisha Usaidizi wa NWC: Hakikisha kuwa programu yako ya mkoba inaoana na Nostr Wallet Connect. -\n\t2. Unda Muunganisho: Nenda kwenye programu yako ya mkoba na unda muunganisho mpya wa NWC. -\n\t3. Unganisha na Amethyst: Mkoba wako kwa kawaida utakupa chaguo tatu: -\n\t\t3.1 Kiungo cha Kiotomatiki: Kitufe cha kufungua na kuunganisha moja kwa moja na Amethyst. Ikiwa ndivyo, bonyeza kitufe hicho, fungua Amethyst na bonyeza hifadhi kwenye skrini mpya. -\n\t\t3.2 Kiungo cha Mkono: URI ya Muunganisho unayoweza kunakili na kubonyeza ikoni ya kubandika kwenye skrini hii kuiingiza moja kwa moja. -\n\t\t3.3 Msimbo wa QR: Mkoba unaunda msimbo wa QR ambao unaweza kuusoma kutoka kwa kitufe cha msimbo wa QR kwenye skrini hii. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst haikupata programu yoyote ya mkoba inayounga mkono Nostr Wallet Connect (NWC). \n\nUnachoweza kufanya: \n\t1. Thibitisha Usaidizi wa NWC: Hakikisha kuwa programu yako ya mkoba inaoana na Nostr Wallet Connect. \n\t2. Unda Muunganisho: Nenda kwenye programu yako ya mkoba na unda muunganisho mpya wa NWC. \n\t3. Unganisha na Amethyst: Mkoba wako kwa kawaida utakupa chaguo tatu: \n\t\t3.1 Kiungo cha Kiotomatiki: Kitufe cha kufungua na kuunganisha moja kwa moja na Amethyst. Ikiwa ndivyo, bonyeza kitufe hicho, fungua Amethyst na bonyeza hifadhi kwenye skrini mpya. \n\t\t3.2 Kiungo cha Mkono: URI ya Muunganisho unayoweza kunakili na kubonyeza ikoni ya kubandika kwenye skrini hii kuiingiza moja kwa moja. \n\t\t3.3 Msimbo wa QR: Mkoba unaunda msimbo wa QR ambao unaweza kuusoma kutoka kwa kitufe cha msimbo wa QR kwenye skrini hii.</string> <string name="create">Unda</string> <string name="d">siku</string> <string name="danger_zone">Eneo Hatari</string> @@ -2686,11 +2669,8 @@ <string name="proxy_section">Relay za Wakala</string> <string name="proxy_section_explainer">Relay za mkusanyaji ambazo programu lazima itumie kupakua malisho yako, kama filter.nostr.wine. Hii inabadilisha mfano wa sanduku la nje na itafanya programu iunganike na relay zilizo kwenye orodha zako tu.</string> <string name="public_chat">Mazungumzo ya Umma</string> - <string name="public_chat_explainer">Mazungumzo ya umma yanaweza kuonekana na kila mtu kwenye Nostr na mtu yeyote - anaweza kushiriki. Ni mazuri kwa jamii wazi kuhusu mada maalum. - Udhibiti unaweza kufanywa kwa kufuta machapisho kwenye relay zake</string> - <string name="public_chat_relays_explainer">Ingiza kati ya relay 1–3 zinazohifadhi kikundi hiki. - Wateja wa Nostr hutumia mipangilio hii kujua mahali pa kupakua ujumbe na kutuma ujumbe wako.</string> + <string name="public_chat_explainer">Mazungumzo ya umma yanaweza kuonekana na kila mtu kwenye Nostr na mtu yeyote anaweza kushiriki. Ni mazuri kwa jamii wazi kuhusu mada maalum. Udhibiti unaweza kufanywa kwa kufuta machapisho kwenye relay zake</string> + <string name="public_chat_relays_explainer">Ingiza kati ya relay 1–3 zinazohifadhi kikundi hiki. Wateja wa Nostr hutumia mipangilio hii kujua mahali pa kupakua ujumbe na kutuma ujumbe wako.</string> <string name="public_chat_title">Metadata ya Mazungumzo ya Umma</string> <string name="public_chats">Mazungumzo ya Umma</string> <string name="public_home_section">Relay za Sanduku la Nje/Nyumbani za Umma</string> @@ -2714,10 +2694,7 @@ <string name="read_only_user">Mtumiaji wa kusoma tu</string> <string name="record_a_message_description">Bonyeza na ushikilie kurekodi ujumbe</string> <string name="relay_chat">Mazungumzo ya Relay</string> - <string name="relay_chat_explainer">Mazungumzo ya relay ni vikundi vya mazungumzo vinavyodhibitiwa na relay yao ya nyumbani. - Yanaweza kuonekana na kila mtu kwenye Nostr na mtu yeyote anaweza kushiriki. - Ni mazuri kwa jamii wazi kuhusu mada maalum. Baadhi ya vikundi hivi ni vya muda mfupi - na hivyo ujumbe wa mazungumzo huisha baada ya muda</string> + <string name="relay_chat_explainer">Mazungumzo ya relay ni vikundi vya mazungumzo vinavyodhibitiwa na relay yao ya nyumbani. Yanaweza kuonekana na kila mtu kwenye Nostr na mtu yeyote anaweza kushiriki. Ni mazuri kwa jamii wazi kuhusu mada maalum. Baadhi ya vikundi hivi ni vya muda mfupi na hivyo ujumbe wa mazungumzo huisha baada ya muda</string> <string name="relay_chat_title">Mazungumzo ya Relay</string> <string name="relay_management_tab_events">Matukio</string> <string name="relay_management_tab_kinds">Aina</string> diff --git a/commonsUI/src/commonMain/composeResources/values-ta-rIN/strings.xml b/commonsUI/src/commonMain/composeResources/values-ta-rIN/strings.xml index 3b46dde816..106be142e3 100644 --- a/commonsUI/src/commonMain/composeResources/values-ta-rIN/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ta-rIN/strings.xml @@ -224,7 +224,7 @@ <string name="copy_user_id">பயனர் IDயை நகலெடுக்கவும்</string> <string name="unblock_user">பயனரைத் தடைநீக்கு</string> <string name="user_is_blocked_hidden">இந்த பயனர் தடுக்கப்பட்டு/முடக்கப்பட்டுள்ளார். அவர்களின் இடுகைகள் மறைக்கப்பட்டுள்ளன.</string> - <string name="npub_hex_username">npub, hex, பயனர்பெயர் </string> + <string name="npub_hex_username">npub, hex, பயனர்பெயர்</string> <string name="clear">அழி</string> <string name="app_logo">பயன்பாட்டு இலச்சினை</string> <string name="nsec_npub_hex_private_key">nsec / npub / hex தனிப்பட்ட சாவி</string> @@ -272,17 +272,10 @@ <string name="mark_all_known_as_read">தெரிந்தவற்றைப் படித்ததாகக் குறி</string> <string name="mark_all_new_as_read">புதியனவற்றைப் படித்ததாகக் குறி</string> <string name="mark_all_as_read">அனைத்தையும் படித்ததாகக் குறி</string> - <string name="account_backup_tips2_md"> ## திறவுகோல் காப்புப்பிரதி மற்றும் பாதுகாப்பு குறிப்புகள் - \n\nஉங்கள் கணக்கு ஒரு இரகசிய திறவுகோலால் பாதுகாக்கப்படுகிறது. இந்த திறவுகோல் **nsec1** என்று தொடங்கும் நீண்ட எழுத்துத் தொடராகும். இந்த இரகசிய திறவுகோலை யாராவது பெற்றால் அவர்கள் உங்கள் அடையாளத்தை பயன்படுத்தி இடுகையிடலாம் மற்றும் மாற்றலாம். - \n\n- உங்கள் இரகசிய திறவுகோலை நீங்கள் நம்பாத எந்த வலைத்தளத்திலும் அல்லது மென்பொருளிலும் வைக்க **வேண்டாம்**. - \n- Amethyst டெவலப்பர்கள் உங்கள் இரகசிய திறவுகோலை **ஒருபோதும்** கேட்கமாட்டார்கள். - \n- கணக்கு மீட்டெடுப்புக்காக உங்கள் இரகசிய திறவுகோலை பாதுகாப்பாக காப்புப்பிரதி எடுங்கள். கடவுச்சொல் மேலாளரை பயன்படுத்த பரிந்துரைக்கிறோம். - </string> - <string name="account_backup_tips3_md"> கூடுதல் பாதுகாப்புக்காக, உங்கள் திறவுகோலை கடவுச்சொல்லால் குறியாக்கலாம். இந்த திறவுகோல் **ncryptsec1** என்று தொடங்கும் மற்றும் உங்கள் கடவுச்சொல்லின்றி பயன்படுத்த முடியாது. - \n\nகடவுச்சொல்லை மறந்தால், உங்கள் திறவுகோலை மீட்டெடுக்க முடியாது. - </string> + <string name="account_backup_tips2_md">## திறவுகோல் காப்புப்பிரதி மற்றும் பாதுகாப்பு குறிப்புகள் \n\nஉங்கள் கணக்கு ஒரு இரகசிய திறவுகோலால் பாதுகாக்கப்படுகிறது. இந்த திறவுகோல் **nsec1** என்று தொடங்கும் நீண்ட எழுத்துத் தொடராகும். இந்த இரகசிய திறவுகோலை யாராவது பெற்றால் அவர்கள் உங்கள் அடையாளத்தை பயன்படுத்தி இடுகையிடலாம் மற்றும் மாற்றலாம். \n\n- உங்கள் இரகசிய திறவுகோலை நீங்கள் நம்பாத எந்த வலைத்தளத்திலும் அல்லது மென்பொருளிலும் வைக்க **வேண்டாம்**. \n- Amethyst டெவலப்பர்கள் உங்கள் இரகசிய திறவுகோலை **ஒருபோதும்** கேட்கமாட்டார்கள். \n- கணக்கு மீட்டெடுப்புக்காக உங்கள் இரகசிய திறவுகோலை பாதுகாப்பாக காப்புப்பிரதி எடுங்கள். கடவுச்சொல் மேலாளரை பயன்படுத்த பரிந்துரைக்கிறோம்.</string> + <string name="account_backup_tips3_md">கூடுதல் பாதுகாப்புக்காக, உங்கள் திறவுகோலை கடவுச்சொல்லால் குறியாக்கலாம். இந்த திறவுகோல் **ncryptsec1** என்று தொடங்கும் மற்றும் உங்கள் கடவுச்சொல்லின்றி பயன்படுத்த முடியாது. \n\nகடவுச்சொல்லை மறந்தால், உங்கள் திறவுகோலை மீட்டெடுக்க முடியாது.</string> <string name="encrypt_and_copy_my_secret_key">என் இரகசிய விசையை மறைகுறியாக்கி நகல் எடுக்கவும்</string> - <string name="badge_award_image_for"> %1$s க்கான பேட்ஜ் விருது படம்</string> + <string name="badge_award_image_for">%1$s க்கான பேட்ஜ் விருது படம்</string> <string name="badge_award_image">"பேட்ஜ் விருது படம்</string> <string name="new_badge_award_notif">நீங்கள் ஒரு புதிய பேட்ஜ் விருதைப் பெற்றீர்கள்</string> <string name="award_granted_to">பேட்ஜ் விருது வழங்கப்பட்டது. பெறுநர்:</string> @@ -304,7 +297,7 @@ <string name="quick_action_block_dialog_btn">தடைசெய்</string> <string name="quick_action_delete_dialog_btn">அழி</string> <string name="quick_action_block">தடைசெய்</string> - <string name="quick_action_report">புகார் </string> + <string name="quick_action_report">புகார்</string> <string name="quick_action_dont_show_again_button">மீண்டும் காட்டாதே</string> <string name="report_dialog_spam">எரிதம் அல்லது மோசடிகள்</string> <string name="report_dialog_profanity">அவதூறு அல்லது வெறுக்கத்தக்க நடத்தை</string> @@ -871,7 +864,7 @@ <string name="followed_tags">பின்பற்றப்படும் சிட்டைகள்</string> <string name="discover_follows_explainer">இவை நீங்கள் மற்றவர்களுக்கு பரிந்துரைக்கும் பயனர்களின் பட்டியல்கள். பொது பயனர்கள் மட்டுமே அனுமதிக்கப்படுவர்.</string> <string name="community_approved_posts">அங்கீகரிக்கப்பட்ட குறிப்பு</string> - <string name="groups_no_descriptor">இந்த அரட்டைக் குழுவின் விளக்கமும் விதிகளும் இன்னும் சேர்க்கபடவில்லை. உரிமையாளரை அணுகி அவற்றை சேர்க்க கோரவும் </string> + <string name="groups_no_descriptor">இந்த அரட்டைக் குழுவின் விளக்கமும் விதிகளும் இன்னும் சேர்க்கபடவில்லை. உரிமையாளரை அணுகி அவற்றை சேர்க்க கோரவும்</string> <string name="community_no_descriptor">இந்த சமூகத்தின் விளக்கமும் விதிகளும் இன்னும் சேர்க்கபடவில்லை. உரிமையாளரை அணுகி அவற்றை சேர்க்க கோரவும்</string> <string name="user_preferences">பயனர் விருப்பங்கள்</string> <string name="settings_search_placeholder">அமைப்புகளை தேடு</string> @@ -965,9 +958,7 @@ <string name="push_server_none_explainer">புஷ் அறிவிப்புகளை முடக்கும்</string> <string name="push_server_uses_app_explainer">%1$s ஆப்பை பயன்படுத்துகிறது</string> <string name="push_server_install_app">புஷ் அறிவிப்பு அமைவு</string> - <string name="push_server_install_app_description"> Push அறிவிப்புகளை பெற, [Unified Push](https://unifiedpush.org/) ஆதரிக்கும் எந்த பயன்பாட்டையும் நிறுவுங்கள், எடுத்துக்காட்டாக [Ntfy](https://ntfy.sh/). - நிறுவிய பின், அமைப்புகளில் நீங்கள் பயன்படுத்த விரும்பும் பயன்பாட்டை தேர்ந்தெடுங்கள். - </string> + <string name="push_server_install_app_description">Push அறிவிப்புகளை பெற, [Unified Push](https://unifiedpush.org/) ஆதரிக்கும் எந்த பயன்பாட்டையும் நிறுவுங்கள், எடுத்துக்காட்டாக [Ntfy](https://ntfy.sh/). நிறுவிய பின், அமைப்புகளில் நீங்கள் பயன்படுத்த விரும்பும் பயன்பாட்டை தேர்ந்தெடுங்கள்.</string> <string name="thread_title">நூல்</string> <string name="send_the_seller_a_message">விற்பனையாளருக்கு ஒரு செய்தி அனுப்பவும்</string> <string name="hi_there_is_this_still_available">வணக்கம், இது இன்னும் கிடைக்கிறதா?</string> @@ -2016,15 +2007,7 @@ <string name="could_not_fetch_invoice_from_details">%1$s இலிருந்து இன்வாய்ஸை பெற முடியவில்லை: %2$s</string> <string name="could_not_open_the_compressed_file">சுருக்கப்பட்ட கோப்பை திறக்க முடியவில்லை</string> <string name="couldnt_find_nwc_wallets">வாலட் கிடைக்கவில்லை</string> - <string name="couldnt_find_nwc_wallets_description">Nostr Wallet Connect (NWC)-ஐ ஆதரிக்கும் எந்த பணப்பை பயன்பாடுகளையும் Amethyst கண்டுபிடிக்கவில்லை. -\n\nநீங்கள் செய்யக்கூடியவை: -\n\t1. NWC ஆதரவை சரிபாருங்கள்: உங்கள் விருப்பமான பணப்பை பயன்பாடு Nostr Wallet Connect-உடன் இணக்கமானதா என இரட்டிப்பு சரிபாருங்கள். -\n\t2. இணைப்பை உருவாக்குங்கள்: உங்கள் பணப்பை பயன்பாட்டிற்கு சென்று புதிய NWC இணைப்பை உருவாக்குங்கள். -\n\t3. Amethyst-உடன் இணைக்குங்கள்: உங்கள் பணப்பை பொதுவாக மூன்று விருப்பங்களை வழங்கும்: -\n\t\t3.1 தானியங்கி இணைப்பு: Amethyst-ஐ தானாக திறந்து இணைக்க ஒரு பொத்தான். அவ்வாறிருந்தால், அந்த பொத்தானை அழுத்தி Amethyst-ஐ திறந்து புதிய திரையில் சேமி அழுத்துங்கள். -\n\t\t3.2 கைமுறை இணைப்பு: நீங்கள் நகலெடுக்கக்கூடிய Connection URI, இந்த திரையில் ஒட்டு ஐகானை அழுத்தி நேரடியாக உள்ளிடலாம். -\n\t\t3.3 QR குறியீடு: பணப்பை ஒரு QR குறியீட்டை உருவாக்கும், இந்த திரையில் QR குறியீடு பொத்தானிலிருந்து படிக்கலாம். - </string> + <string name="couldnt_find_nwc_wallets_description">Nostr Wallet Connect (NWC)-ஐ ஆதரிக்கும் எந்த பணப்பை பயன்பாடுகளையும் Amethyst கண்டுபிடிக்கவில்லை. \n\nநீங்கள் செய்யக்கூடியவை: \n\t1. NWC ஆதரவை சரிபாருங்கள்: உங்கள் விருப்பமான பணப்பை பயன்பாடு Nostr Wallet Connect-உடன் இணக்கமானதா என இரட்டிப்பு சரிபாருங்கள். \n\t2. இணைப்பை உருவாக்குங்கள்: உங்கள் பணப்பை பயன்பாட்டிற்கு சென்று புதிய NWC இணைப்பை உருவாக்குங்கள். \n\t3. Amethyst-உடன் இணைக்குங்கள்: உங்கள் பணப்பை பொதுவாக மூன்று விருப்பங்களை வழங்கும்: \n\t\t3.1 தானியங்கி இணைப்பு: Amethyst-ஐ தானாக திறந்து இணைக்க ஒரு பொத்தான். அவ்வாறிருந்தால், அந்த பொத்தானை அழுத்தி Amethyst-ஐ திறந்து புதிய திரையில் சேமி அழுத்துங்கள். \n\t\t3.2 கைமுறை இணைப்பு: நீங்கள் நகலெடுக்கக்கூடிய Connection URI, இந்த திரையில் ஒட்டு ஐகானை அழுத்தி நேரடியாக உள்ளிடலாம். \n\t\t3.3 QR குறியீடு: பணப்பை ஒரு QR குறியீட்டை உருவாக்கும், இந்த திரையில் QR குறியீடு பொத்தானிலிருந்து படிக்கலாம்.</string> <string name="create">உருவாக்கு</string> <string name="d">நா</string> <string name="danger_zone">ஆபத்து மண்டலம்</string> @@ -2416,7 +2399,7 @@ <string name="metadata_strip_failed_upload_cancelled">மீடியாவிலிருந்து தனிப்பட்ட மெட்டாடேட்டாவை நீக்கத் தோல்வியடைந்தது. பதிவேற்றம் ரத்துசெய்யப்பட்டது.</string> <string name="migrate_bookmarks_success">புக்மார்க்குகள் வெற்றிகரமாக இடம்பெயர்க்கப்பட்டன</string> <string name="missing_lud16">Lightning அமைப்பு இல்லை</string> - <string name="more_options">மேலும் </string> + <string name="more_options">மேலும்</string> <string name="move_bookmark_to_private_label">தனிப்பட்டதிற்கு நகர்த்து</string> <string name="move_bookmark_to_public_label">பொதுவிற்கு நகர்த்து</string> <string name="music_playlist_cover_upload_cta">அட்டை படத்தை பதிவேற்று</string> @@ -2648,7 +2631,7 @@ <item quantity="one">இந்த இடுகையில் %1$d hashtag-ஐ விட அதிகம் உள்ளது</item> <item quantity="other">இந்த இடுகையில் %1$d hashtags-ஐ விட அதிகம் உள்ளது</item> </plurals> - <string name="preview_card_image_for"> %1$s க்கான முன்னோட்ட அட்டைப் படம்</string> + <string name="preview_card_image_for">%1$s க்கான முன்னோட்ட அட்டைப் படம்</string> <string name="privacy_options">தனியுரிமை விருப்பங்கள்</string> <string name="privacy_policy">தனியுரிமைக் கொள்கை</string> <string name="private_conversation_notification">தனிப்பட்ட செய்தியை மறைக்குறியாக்க இயலவில்லை \n \n %1$s மற்றும் %2$s க்கு இடையே தனிப்பட்ட/ குறியாக்கப்பட்ட உரையாடலில் நீங்கள் மேற்கோள் காட்டப்பட்டீர்கள்.</string> @@ -2673,11 +2656,8 @@ <string name="propose_an_edit">திருத்தத்தை முன்மொழிக</string> <string name="proxy_section_explainer">filter.nostr.wine போன்ற ஒருங்கிணைப்பு relays, பயன்பாடு உங்கள் ஊட்டங்களை பதிவிறக்க பயன்படுத்த வேண்டும். இது அவுட்பாக்ஸ் மாதிரியை மாற்றுகிறது, பயன்பாட்டை உங்கள் பட்டியல்களில் உள்ள relays மட்டுமே இணைக்கும்.</string> <string name="public_chat">பொது அரட்டை</string> - <string name="public_chat_explainer">பொது அரட்டைகள் Nostr-ல் அனைவருக்கும் தெரியும் மற்றும் யாரும் - அவற்றில் பங்கேற்கலாம். அவை குறிப்பிட்ட தலைப்புகளை சுற்றிய திறந்த சமூகங்களுக்கு மிகவும் ஏற்றவை. - அதன் relays-ல் இடுகைகளை நீக்குவதன் மூலம் மிதவாண்மையை கட்டுப்படுத்தலாம்</string> - <string name="public_chat_relays_explainer">இந்த குழுவை ஹோஸ்ட் செய்யும் 1–3 relays-ஐ உள்ளிடவும். - Nostr கிளையன்கள் செய்திகளை எங்கிருந்து பதிவிறக்கம் செய்வது மற்றும் உங்கள் செய்திகளை எங்கே அனுப்புவது என்பதை அறிய இந்த அமைப்பை பயன்படுத்துகின்றன.</string> + <string name="public_chat_explainer">பொது அரட்டைகள் Nostr-ல் அனைவருக்கும் தெரியும் மற்றும் யாரும் அவற்றில் பங்கேற்கலாம். அவை குறிப்பிட்ட தலைப்புகளை சுற்றிய திறந்த சமூகங்களுக்கு மிகவும் ஏற்றவை. அதன் relays-ல் இடுகைகளை நீக்குவதன் மூலம் மிதவாண்மையை கட்டுப்படுத்தலாம்</string> + <string name="public_chat_relays_explainer">இந்த குழுவை ஹோஸ்ட் செய்யும் 1–3 relays-ஐ உள்ளிடவும். Nostr கிளையன்கள் செய்திகளை எங்கிருந்து பதிவிறக்கம் செய்வது மற்றும் உங்கள் செய்திகளை எங்கே அனுப்புவது என்பதை அறிய இந்த அமைப்பை பயன்படுத்துகின்றன.</string> <string name="public_chat_relays_title">Relay கள்</string> <string name="public_chat_title">பொது அரட்டை மெட்டாடேட்டா</string> <string name="public_chats">பொது அரட்டைகள்</string> @@ -2702,10 +2682,7 @@ <string name="read_only_user">படிக்க மட்டும் பயனர்</string> <string name="record_a_message_description">செய்தி பதிவு செய்ய கிளிக் செய்து பிடிக்கவும்</string> <string name="relay_chat">Relay அரட்டை</string> - <string name="relay_chat_explainer">Relay அரட்டைகள் என்பவை அவற்றின் முகப்பு relay-ஆல் கட்டுப்படுத்தப்படும் அரட்டை குழுக்கள். - அவை Nostr-ல் அனைவருக்கும் தெரியும் மற்றும் யாரும் அவற்றில் பங்கேற்கலாம். - அவை குறிப்பிட்ட தலைப்புகளை சுற்றிய திறந்த சமூகங்களுக்கு மிகவும் ஏற்றவை. இந்த குழுக்களில் சில தற்காலிகமானவை - எனவே அரட்டை செய்திகள் காலப்போக்கில் மறைந்துவிடும்</string> + <string name="relay_chat_explainer">Relay அரட்டைகள் என்பவை அவற்றின் முகப்பு relay-ஆல் கட்டுப்படுத்தப்படும் அரட்டை குழுக்கள். அவை Nostr-ல் அனைவருக்கும் தெரியும் மற்றும் யாரும் அவற்றில் பங்கேற்கலாம். அவை குறிப்பிட்ட தலைப்புகளை சுற்றிய திறந்த சமூகங்களுக்கு மிகவும் ஏற்றவை. இந்த குழுக்களில் சில தற்காலிகமானவை எனவே அரட்டை செய்திகள் காலப்போக்கில் மறைந்துவிடும்</string> <string name="relay_chat_title">Relay அரட்டைகள்</string> <string name="relay_management_tab_events">நிகழ்வுகள்</string> <string name="relay_management_tab_kinds">வகைகள்</string> diff --git a/commonsUI/src/commonMain/composeResources/values-ta/strings.xml b/commonsUI/src/commonMain/composeResources/values-ta/strings.xml index 18a9e71315..210f73485f 100644 --- a/commonsUI/src/commonMain/composeResources/values-ta/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ta/strings.xml @@ -211,7 +211,7 @@ <string name="copy_user_id">பயனர் IDயை நகலெடுக்கவும்</string> <string name="unblock_user">பயனரைத் தடைநீக்கு</string> <string name="user_is_blocked_hidden">இந்த பயனர் தடுக்கப்பட்டு/முடக்கப்பட்டுள்ளார். அவர்களின் இடுகைகள் மறைக்கப்பட்டுள்ளன.</string> - <string name="npub_hex_username">npub, hex, பயனர்பெயர் </string> + <string name="npub_hex_username">npub, hex, பயனர்பெயர்</string> <string name="clear">அழி</string> <string name="app_logo">பயன்பாட்டு இலச்சினை</string> <string name="nsec_npub_hex_private_key">nsec / npub / hex தனிப்பட்ட சாவி</string> @@ -259,19 +259,10 @@ <string name="mark_all_known_as_read">தெரிந்தவற்றைப் படித்ததாகக் குறி</string> <string name="mark_all_new_as_read">புதியனவற்றைப் படித்ததாகக் குறி</string> <string name="mark_all_as_read">அனைத்தையும் படித்ததாகக் குறி</string> - <string name="account_backup_tips2_md"> - ## திறவுகோல் காப்புப்பிரதி மற்றும் பாதுகாப்பு குறிப்புகள் - \n\nஉங்கள் கணக்கு ஒரு இரகசிய திறவுகோலால் பாதுகாக்கப்படுகிறது. இந்த திறவுகோல் **nsec1** என்று தொடங்கும் நீண்ட எழுத்துத் தொடராகும். இந்த இரகசிய திறவுகோலை யாராவது பெற்றால் அவர்கள் உங்கள் அடையாளத்தை பயன்படுத்தி இடுகையிடலாம் மற்றும் மாற்றலாம். - \n\n- உங்கள் இரகசிய திறவுகோலை நீங்கள் நம்பாத எந்த வலைத்தளத்திலும் அல்லது மென்பொருளிலும் வைக்க **வேண்டாம்**. - \n- Amethyst டெவலப்பர்கள் உங்கள் இரகசிய திறவுகோலை **ஒருபோதும்** கேட்கமாட்டார்கள். - \n- கணக்கு மீட்டெடுப்புக்காக உங்கள் இரகசிய திறவுகோலை பாதுகாப்பாக காப்புப்பிரதி எடுங்கள். கடவுச்சொல் மேலாளரை பயன்படுத்த பரிந்துரைக்கிறோம். - </string> - <string name="account_backup_tips3_md"> - கூடுதல் பாதுகாப்புக்காக, உங்கள் திறவுகோலை கடவுச்சொல்லால் குறியாக்கலாம். இந்த திறவுகோல் **ncryptsec1** என்று தொடங்கும் மற்றும் உங்கள் கடவுச்சொல்லின்றி பயன்படுத்த முடியாது. - \n\nகடவுச்சொல்லை மறந்தால், உங்கள் திறவுகோலை மீட்டெடுக்க முடியாது. - </string> + <string name="account_backup_tips2_md">## திறவுகோல் காப்புப்பிரதி மற்றும் பாதுகாப்பு குறிப்புகள் \n\nஉங்கள் கணக்கு ஒரு இரகசிய திறவுகோலால் பாதுகாக்கப்படுகிறது. இந்த திறவுகோல் **nsec1** என்று தொடங்கும் நீண்ட எழுத்துத் தொடராகும். இந்த இரகசிய திறவுகோலை யாராவது பெற்றால் அவர்கள் உங்கள் அடையாளத்தை பயன்படுத்தி இடுகையிடலாம் மற்றும் மாற்றலாம். \n\n- உங்கள் இரகசிய திறவுகோலை நீங்கள் நம்பாத எந்த வலைத்தளத்திலும் அல்லது மென்பொருளிலும் வைக்க **வேண்டாம்**. \n- Amethyst டெவலப்பர்கள் உங்கள் இரகசிய திறவுகோலை **ஒருபோதும்** கேட்கமாட்டார்கள். \n- கணக்கு மீட்டெடுப்புக்காக உங்கள் இரகசிய திறவுகோலை பாதுகாப்பாக காப்புப்பிரதி எடுங்கள். கடவுச்சொல் மேலாளரை பயன்படுத்த பரிந்துரைக்கிறோம்.</string> + <string name="account_backup_tips3_md">கூடுதல் பாதுகாப்புக்காக, உங்கள் திறவுகோலை கடவுச்சொல்லால் குறியாக்கலாம். இந்த திறவுகோல் **ncryptsec1** என்று தொடங்கும் மற்றும் உங்கள் கடவுச்சொல்லின்றி பயன்படுத்த முடியாது. \n\nகடவுச்சொல்லை மறந்தால், உங்கள் திறவுகோலை மீட்டெடுக்க முடியாது.</string> <string name="encrypt_and_copy_my_secret_key">என் இரகசிய விசையை மறைகுறியாக்கி நகல் எடுக்கவும்</string> - <string name="badge_award_image_for"> %1$s க்கான பேட்ஜ் விருது படம்</string> + <string name="badge_award_image_for">%1$s க்கான பேட்ஜ் விருது படம்</string> <string name="badge_award_image">"பேட்ஜ் விருது படம்</string> <string name="new_badge_award_notif">நீங்கள் ஒரு புதிய பேட்ஜ் விருதைப் பெற்றீர்கள்</string> <string name="award_granted_to">பேட்ஜ் விருது வழங்கப்பட்டது. பெறுநர்:</string> @@ -293,7 +284,7 @@ <string name="quick_action_block_dialog_btn">தடைசெய்</string> <string name="quick_action_delete_dialog_btn">அழி</string> <string name="quick_action_block">தடைசெய்</string> - <string name="quick_action_report">புகார் </string> + <string name="quick_action_report">புகார்</string> <string name="quick_action_dont_show_again_button">மீண்டும் காட்டாதே</string> <string name="report_dialog_spam">எரிதம் அல்லது மோசடிகள்</string> <string name="report_dialog_profanity">அவதூறு அல்லது வெறுக்கத்தக்க நடத்தை</string> @@ -867,7 +858,7 @@ <string name="followed_tags">பின்பற்றப்படும் சிட்டைகள்</string> <string name="discover_follows_explainer">இவை நீங்கள் மற்றவர்களுக்கு பரிந்துரைக்கும் பயனர்களின் பட்டியல்கள். பொது பயனர்கள் மட்டுமே அனுமதிக்கப்படுவர்.</string> <string name="community_approved_posts">அங்கீகரிக்கப்பட்ட குறிப்பு</string> - <string name="groups_no_descriptor">இந்த அரட்டைக் குழுவின் விளக்கமும் விதிகளும் இன்னும் சேர்க்கபடவில்லை. உரிமையாளரை அணுகி அவற்றை சேர்க்க கோரவும் </string> + <string name="groups_no_descriptor">இந்த அரட்டைக் குழுவின் விளக்கமும் விதிகளும் இன்னும் சேர்க்கபடவில்லை. உரிமையாளரை அணுகி அவற்றை சேர்க்க கோரவும்</string> <string name="community_no_descriptor">இந்த சமூகத்தின் விளக்கமும் விதிகளும் இன்னும் சேர்க்கபடவில்லை. உரிமையாளரை அணுகி அவற்றை சேர்க்க கோரவும்</string> <string name="user_preferences">பயனர் விருப்பங்கள்</string> <string name="settings_search_placeholder">அமைப்புகளை தேடு</string> @@ -962,10 +953,7 @@ <string name="push_server_none_explainer">புஷ் அறிவிப்புகளை முடக்கும்</string> <string name="push_server_uses_app_explainer">%1$s ஆப்பை பயன்படுத்துகிறது</string> <string name="push_server_install_app">புஷ் அறிவிப்பு அமைவு</string> - <string name="push_server_install_app_description"> - Push அறிவிப்புகளை பெற, [Unified Push](https://unifiedpush.org/) ஆதரிக்கும் எந்த பயன்பாட்டையும் நிறுவுங்கள், எடுத்துக்காட்டாக [Ntfy](https://ntfy.sh/). - நிறுவிய பின், அமைப்புகளில் நீங்கள் பயன்படுத்த விரும்பும் பயன்பாட்டை தேர்ந்தெடுங்கள். - </string> + <string name="push_server_install_app_description">Push அறிவிப்புகளை பெற, [Unified Push](https://unifiedpush.org/) ஆதரிக்கும் எந்த பயன்பாட்டையும் நிறுவுங்கள், எடுத்துக்காட்டாக [Ntfy](https://ntfy.sh/). நிறுவிய பின், அமைப்புகளில் நீங்கள் பயன்படுத்த விரும்பும் பயன்பாட்டை தேர்ந்தெடுங்கள்.</string> <string name="thread_title">நூல்</string> <string name="send_the_seller_a_message">விற்பனையாளருக்கு ஒரு செய்தி அனுப்பவும்</string> <string name="hi_there_is_this_still_available">வணக்கம், இது இன்னும் கிடைக்கிறதா?</string> @@ -2058,15 +2046,7 @@ <string name="could_not_fetch_invoice_from_details">%1$s இலிருந்து இன்வாய்ஸை பெற முடியவில்லை: %2$s</string> <string name="could_not_open_the_compressed_file">சுருக்கப்பட்ட கோப்பை திறக்க முடியவில்லை</string> <string name="couldnt_find_nwc_wallets">வாலட் கிடைக்கவில்லை</string> - <string name="couldnt_find_nwc_wallets_description">Nostr Wallet Connect (NWC)-ஐ ஆதரிக்கும் எந்த பணப்பை பயன்பாடுகளையும் Amethyst கண்டுபிடிக்கவில்லை. -\n\nநீங்கள் செய்யக்கூடியவை: -\n\t1. NWC ஆதரவை சரிபாருங்கள்: உங்கள் விருப்பமான பணப்பை பயன்பாடு Nostr Wallet Connect-உடன் இணக்கமானதா என இரட்டிப்பு சரிபாருங்கள். -\n\t2. இணைப்பை உருவாக்குங்கள்: உங்கள் பணப்பை பயன்பாட்டிற்கு சென்று புதிய NWC இணைப்பை உருவாக்குங்கள். -\n\t3. Amethyst-உடன் இணைக்குங்கள்: உங்கள் பணப்பை பொதுவாக மூன்று விருப்பங்களை வழங்கும்: -\n\t\t3.1 தானியங்கி இணைப்பு: Amethyst-ஐ தானாக திறந்து இணைக்க ஒரு பொத்தான். அவ்வாறிருந்தால், அந்த பொத்தானை அழுத்தி Amethyst-ஐ திறந்து புதிய திரையில் சேமி அழுத்துங்கள். -\n\t\t3.2 கைமுறை இணைப்பு: நீங்கள் நகலெடுக்கக்கூடிய Connection URI, இந்த திரையில் ஒட்டு ஐகானை அழுத்தி நேரடியாக உள்ளிடலாம். -\n\t\t3.3 QR குறியீடு: பணப்பை ஒரு QR குறியீட்டை உருவாக்கும், இந்த திரையில் QR குறியீடு பொத்தானிலிருந்து படிக்கலாம். - </string> + <string name="couldnt_find_nwc_wallets_description">Nostr Wallet Connect (NWC)-ஐ ஆதரிக்கும் எந்த பணப்பை பயன்பாடுகளையும் Amethyst கண்டுபிடிக்கவில்லை. \n\nநீங்கள் செய்யக்கூடியவை: \n\t1. NWC ஆதரவை சரிபாருங்கள்: உங்கள் விருப்பமான பணப்பை பயன்பாடு Nostr Wallet Connect-உடன் இணக்கமானதா என இரட்டிப்பு சரிபாருங்கள். \n\t2. இணைப்பை உருவாக்குங்கள்: உங்கள் பணப்பை பயன்பாட்டிற்கு சென்று புதிய NWC இணைப்பை உருவாக்குங்கள். \n\t3. Amethyst-உடன் இணைக்குங்கள்: உங்கள் பணப்பை பொதுவாக மூன்று விருப்பங்களை வழங்கும்: \n\t\t3.1 தானியங்கி இணைப்பு: Amethyst-ஐ தானாக திறந்து இணைக்க ஒரு பொத்தான். அவ்வாறிருந்தால், அந்த பொத்தானை அழுத்தி Amethyst-ஐ திறந்து புதிய திரையில் சேமி அழுத்துங்கள். \n\t\t3.2 கைமுறை இணைப்பு: நீங்கள் நகலெடுக்கக்கூடிய Connection URI, இந்த திரையில் ஒட்டு ஐகானை அழுத்தி நேரடியாக உள்ளிடலாம். \n\t\t3.3 QR குறியீடு: பணப்பை ஒரு QR குறியீட்டை உருவாக்கும், இந்த திரையில் QR குறியீடு பொத்தானிலிருந்து படிக்கலாம்.</string> <string name="create">உருவாக்கு</string> <string name="d">நா</string> <string name="danger_zone">ஆபத்து மண்டலம்</string> @@ -2486,7 +2466,7 @@ <string name="metadata_strip_failed_upload_cancelled">மீடியாவிலிருந்து தனிப்பட்ட மெட்டாடேட்டாவை நீக்கத் தோல்வியடைந்தது. பதிவேற்றம் ரத்துசெய்யப்பட்டது.</string> <string name="migrate_bookmarks_success">புக்மார்க்குகள் வெற்றிகரமாக இடம்பெயர்க்கப்பட்டன</string> <string name="missing_lud16">Lightning அமைப்பு இல்லை</string> - <string name="more_options">மேலும் </string> + <string name="more_options">மேலும்</string> <string name="move_bookmark_to_private_label">தனிப்பட்டதிற்கு நகர்த்து</string> <string name="move_bookmark_to_public_label">பொதுவிற்கு நகர்த்து</string> <string name="music_playlist_cover_upload_cta">அட்டை படத்தை பதிவேற்று</string> @@ -2728,7 +2708,7 @@ <item quantity="one">இந்த இடுகையில் %1$d hashtag-ஐ விட அதிகம் உள்ளது</item> <item quantity="other">இந்த இடுகையில் %1$d hashtags-ஐ விட அதிகம் உள்ளது</item> </plurals> - <string name="preview_card_image_for"> %1$s க்கான முன்னோட்ட அட்டைப் படம்</string> + <string name="preview_card_image_for">%1$s க்கான முன்னோட்ட அட்டைப் படம்</string> <string name="privacy_options">தனியுரிமை விருப்பங்கள்</string> <string name="privacy_policy">தனியுரிமைக் கொள்கை</string> <string name="private_conversation_notification">தனிப்பட்ட செய்தியை மறைக்குறியாக்க இயலவில்லை \n \n %1$s மற்றும் %2$s க்கு இடையே தனிப்பட்ட/ குறியாக்கப்பட்ட உரையாடலில் நீங்கள் மேற்கோள் காட்டப்பட்டீர்கள்.</string> @@ -2754,11 +2734,8 @@ <string name="proxy_section">Proxy Relays</string> <string name="proxy_section_explainer">filter.nostr.wine போன்ற ஒருங்கிணைப்பு relays, பயன்பாடு உங்கள் ஊட்டங்களை பதிவிறக்க பயன்படுத்த வேண்டும். இது அவுட்பாக்ஸ் மாதிரியை மாற்றுகிறது, பயன்பாட்டை உங்கள் பட்டியல்களில் உள்ள relays மட்டுமே இணைக்கும்.</string> <string name="public_chat">பொது அரட்டை</string> - <string name="public_chat_explainer">பொது அரட்டைகள் Nostr-ல் அனைவருக்கும் தெரியும் மற்றும் யாரும் - அவற்றில் பங்கேற்கலாம். அவை குறிப்பிட்ட தலைப்புகளை சுற்றிய திறந்த சமூகங்களுக்கு மிகவும் ஏற்றவை. - அதன் relays-ல் இடுகைகளை நீக்குவதன் மூலம் மிதவாண்மையை கட்டுப்படுத்தலாம்</string> - <string name="public_chat_relays_explainer">இந்த குழுவை ஹோஸ்ட் செய்யும் 1–3 relays-ஐ உள்ளிடவும். - Nostr கிளையன்கள் செய்திகளை எங்கிருந்து பதிவிறக்கம் செய்வது மற்றும் உங்கள் செய்திகளை எங்கே அனுப்புவது என்பதை அறிய இந்த அமைப்பை பயன்படுத்துகின்றன.</string> + <string name="public_chat_explainer">பொது அரட்டைகள் Nostr-ல் அனைவருக்கும் தெரியும் மற்றும் யாரும் அவற்றில் பங்கேற்கலாம். அவை குறிப்பிட்ட தலைப்புகளை சுற்றிய திறந்த சமூகங்களுக்கு மிகவும் ஏற்றவை. அதன் relays-ல் இடுகைகளை நீக்குவதன் மூலம் மிதவாண்மையை கட்டுப்படுத்தலாம்</string> + <string name="public_chat_relays_explainer">இந்த குழுவை ஹோஸ்ட் செய்யும் 1–3 relays-ஐ உள்ளிடவும். Nostr கிளையன்கள் செய்திகளை எங்கிருந்து பதிவிறக்கம் செய்வது மற்றும் உங்கள் செய்திகளை எங்கே அனுப்புவது என்பதை அறிய இந்த அமைப்பை பயன்படுத்துகின்றன.</string> <string name="public_chat_relays_title">Relay கள்</string> <string name="public_chat_title">பொது அரட்டை மெட்டாடேட்டா</string> <string name="public_chats">பொது அரட்டைகள்</string> @@ -2783,10 +2760,7 @@ <string name="read_only_user">படிக்க மட்டும் பயனர்</string> <string name="record_a_message_description">செய்தி பதிவு செய்ய கிளிக் செய்து பிடிக்கவும்</string> <string name="relay_chat">Relay அரட்டை</string> - <string name="relay_chat_explainer">Relay அரட்டைகள் என்பவை அவற்றின் முகப்பு relay-ஆல் கட்டுப்படுத்தப்படும் அரட்டை குழுக்கள். - அவை Nostr-ல் அனைவருக்கும் தெரியும் மற்றும் யாரும் அவற்றில் பங்கேற்கலாம். - அவை குறிப்பிட்ட தலைப்புகளை சுற்றிய திறந்த சமூகங்களுக்கு மிகவும் ஏற்றவை. இந்த குழுக்களில் சில தற்காலிகமானவை - எனவே அரட்டை செய்திகள் காலப்போக்கில் மறைந்துவிடும்</string> + <string name="relay_chat_explainer">Relay அரட்டைகள் என்பவை அவற்றின் முகப்பு relay-ஆல் கட்டுப்படுத்தப்படும் அரட்டை குழுக்கள். அவை Nostr-ல் அனைவருக்கும் தெரியும் மற்றும் யாரும் அவற்றில் பங்கேற்கலாம். அவை குறிப்பிட்ட தலைப்புகளை சுற்றிய திறந்த சமூகங்களுக்கு மிகவும் ஏற்றவை. இந்த குழுக்களில் சில தற்காலிகமானவை எனவே அரட்டை செய்திகள் காலப்போக்கில் மறைந்துவிடும்</string> <string name="relay_chat_title">Relay அரட்டைகள்</string> <string name="relay_management_tab_events">நிகழ்வுகள்</string> <string name="relay_management_tab_ips">IPs</string> diff --git a/commonsUI/src/commonMain/composeResources/values-th-rTH/strings.xml b/commonsUI/src/commonMain/composeResources/values-th-rTH/strings.xml index 79bd7785d6..ae6a8e1778 100644 --- a/commonsUI/src/commonMain/composeResources/values-th-rTH/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-th-rTH/strings.xml @@ -158,7 +158,7 @@ <string name="send_payment_min_onchain">การ zap แบบ on-chain ต้องใช้อย่างน้อย %1$s sats</string> <string name="send_payment_cashu_insufficient">ยอดคงเหลือใน mint ที่ผู้รับยอมรับไม่เพียงพอ เติมเงินในกระเป๋า Cashu ก่อน</string> <string name="send_payment_onchain_fee">ค่าธรรมเนียมเครือข่าย</string> - <string name="lightning_tips">ส่งทิปให้เบา ๆ </string> + <string name="lightning_tips">ส่งทิปให้เบา ๆ</string> <string name="note_to_receiver">โน้ตถึงผู้รับ</string> <string name="thank_you_so_much">ขอบคุณมาก!</string> <string name="amount_in_sats">จำนวนในหน่วย sat</string> @@ -269,14 +269,8 @@ <string name="mark_all_known_as_read">ทำเครื่องหมายอ่านแล้วในแชทที่รู้จักทั้งหมด</string> <string name="mark_all_new_as_read">ทำเครื่องหมายว่าอ่านแล้วทั้งหมดสำหรับข้อความใหม่</string> <string name="mark_all_as_read">ทพเครื่องหมายว่าอ่านแล้วทั้งหมด</string> - <string name="account_backup_tips2_md"> ## เคล็ดลับการสำรองข้อมูลและความปลอดภัยที่สำคัญ - \n\nบัญชีของคุณได้รับการรักษาความปลอดภัยด้วยคีย์ลับ คีย์เป็นรหัสชุดตัวอักษรยาวขึ้นต้นด้วย **nsec1** ใครก็ตามที่สามารถเข้าถึงรหัสชุดนี้ได้ จะสามารถโพสต์และเปลี่ยนแปลงข้อมูลประจำตัวของคุณได้ - - \n\n- **ห้าม** ใส่รหัสชุดนี้ในเว็บไซต์หรือซอฟต์แวร์ที่คุณไม่ไว้ใจ - \n- ทีมพัฒนา Amethyst **จะไม่มีวัน** ขอรหัสชุดนี้ของคุณ - \n- **ควร** สำรองข้อมูลรหัสชุดนี้ไว้เพื่อกู้คืนบัญชีของคุณ เราแนะนำให้ใช้โปรแกรมจัดการรหัสผ่าน</string> - <string name="account_backup_tips3_md"> สำหรับการรักษาความปลอดภัยเพิ่มเติม คุณสามารถเข้ารหัสคีย์ของคุณด้วยรหัสผ่าน คีย์นี้ขึ้นต้นด้วย **ncryptsec1** และไม่สามารถใช้งานได้หากไม่มีรหัสผ่านของคุณ - \n\n**แต่** หากคุณสูญหายรหัสผ่าน คุณจะไม่สามารถกู้คืนคีย์ของคุณได้ </string> + <string name="account_backup_tips2_md">## เคล็ดลับการสำรองข้อมูลและความปลอดภัยที่สำคัญ \n\nบัญชีของคุณได้รับการรักษาความปลอดภัยด้วยคีย์ลับ คีย์เป็นรหัสชุดตัวอักษรยาวขึ้นต้นด้วย **nsec1** ใครก็ตามที่สามารถเข้าถึงรหัสชุดนี้ได้ จะสามารถโพสต์และเปลี่ยนแปลงข้อมูลประจำตัวของคุณได้ \n\n- **ห้าม** ใส่รหัสชุดนี้ในเว็บไซต์หรือซอฟต์แวร์ที่คุณไม่ไว้ใจ \n- ทีมพัฒนา Amethyst **จะไม่มีวัน** ขอรหัสชุดนี้ของคุณ \n- **ควร** สำรองข้อมูลรหัสชุดนี้ไว้เพื่อกู้คืนบัญชีของคุณ เราแนะนำให้ใช้โปรแกรมจัดการรหัสผ่าน</string> + <string name="account_backup_tips3_md">สำหรับการรักษาความปลอดภัยเพิ่มเติม คุณสามารถเข้ารหัสคีย์ของคุณด้วยรหัสผ่าน คีย์นี้ขึ้นต้นด้วย **ncryptsec1** และไม่สามารถใช้งานได้หากไม่มีรหัสผ่านของคุณ \n\n**แต่** หากคุณสูญหายรหัสผ่าน คุณจะไม่สามารถกู้คืนคีย์ของคุณได้</string> <string name="encrypt_and_copy_my_secret_key">เข้ารหัสและคัดลอกคีย์ความปลอดภัยของฉัน</string> <string name="badge_award_image_for">ภาพเหรียญตราสําหรับ %1$s</string> <string name="badge_award_image">"รูปภาพรางวัลป้าย</string> @@ -1831,7 +1825,7 @@ <string name="back">ย้อนกลับ</string> <string name="backup_keys">สำรองข้อมูลรหัสลับ</string> <string name="badges">แบดจ์</string> - <string name="biometric_authentication_failed"> เกิดข้อผิดพลาดในการตรวจสอบ</string> + <string name="biometric_authentication_failed">เกิดข้อผิดพลาดในการตรวจสอบ</string> <string name="biometric_authentication_failed_explainer">เกิดข้อผิดพลาดจากการยืนยันตนด้วยชีวภาพ</string> <string name="biometric_authentication_failed_explainer_with_error">เกิดข้อผิดพลาดจากการยืนยันตนด้วยชีวภาพ: Error: %1$s</string> <string name="block_only">บล๊อก</string> @@ -1894,7 +1888,7 @@ <string name="cashu">โทเคน Cashu</string> <string name="cashu_checking_mint">กำลังตรวจสอบ mint…</string> <string name="cashu_failed_redemption">ไม่สามารถใช้งาน Cashu</string> - <string name="cashu_failed_redemption_explainer_error_msg">ผู้ให้บริการ Mint เกิดข้อผิดพลาด : %1$s </string> + <string name="cashu_failed_redemption_explainer_error_msg">ผู้ให้บริการ Mint เกิดข้อผิดพลาด : %1$s</string> <string name="cashu_history_incoming">รับแล้ว</string> <string name="cashu_history_outgoing">ส่งแล้ว</string> <string name="cashu_mint_matches_header">Mints ที่ตรงกัน</string> @@ -1986,15 +1980,7 @@ <string name="could_not_fetch_invoice_from_details">ไม่สามารถดึงใบแจ้งหนี้จาก %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">ไม่สามารถเปิดดูข้อมูลที่ถูกบีบอัดได้</string> <string name="couldnt_find_nwc_wallets">ไม่พบกระเป๋าเงิน</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst ไม่พบแอปกระเป๋าเงินที่รองรับ Nostr Wallet Connect (NWC) -\n\nสิ่งที่คุณทำได้: -\n\t1. ตรวจสอบการรองรับ NWC: ตรวจสอบอีกครั้งว่าแอปกระเป๋าเงินที่คุณต้องการใช้รองรับ Nostr Wallet Connect หรือไม่ -\n\t2. สร้างการเชื่อมต่อ: ไปที่แอปกระเป๋าเงินของคุณและสร้างการเชื่อมต่อ NWC ใหม่ -\n\t3. เชื่อมกับ Amethyst: กระเป๋าเงินของคุณมักจะให้ตัวเลือกสามแบบ: -\n\t\t3.1 ลิงก์อัตโนมัติ: ปุ่มเพื่อเปิดและเชื่อมต่อกับ Amethyst โดยอัตโนมัติ หากมี ให้กดปุ่มนั้น เปิด Amethyst และกดบันทึกที่หน้าจอใหม่ -\n\t\t3.2 ลิงก์ด้วยตนเอง: URI การเชื่อมต่อที่คุณสามารถคัดลอกและกดไอคอนวางบนหน้าจอนี้เพื่อป้อนโดยตรง -\n\t\t3.3 QR Code: กระเป๋าเงินสร้าง QR Code ที่คุณสามารถอ่านได้จากปุ่ม QR code ในหน้าจอนี้ - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst ไม่พบแอปกระเป๋าเงินที่รองรับ Nostr Wallet Connect (NWC) \n\nสิ่งที่คุณทำได้: \n\t1. ตรวจสอบการรองรับ NWC: ตรวจสอบอีกครั้งว่าแอปกระเป๋าเงินที่คุณต้องการใช้รองรับ Nostr Wallet Connect หรือไม่ \n\t2. สร้างการเชื่อมต่อ: ไปที่แอปกระเป๋าเงินของคุณและสร้างการเชื่อมต่อ NWC ใหม่ \n\t3. เชื่อมกับ Amethyst: กระเป๋าเงินของคุณมักจะให้ตัวเลือกสามแบบ: \n\t\t3.1 ลิงก์อัตโนมัติ: ปุ่มเพื่อเปิดและเชื่อมต่อกับ Amethyst โดยอัตโนมัติ หากมี ให้กดปุ่มนั้น เปิด Amethyst และกดบันทึกที่หน้าจอใหม่ \n\t\t3.2 ลิงก์ด้วยตนเอง: URI การเชื่อมต่อที่คุณสามารถคัดลอกและกดไอคอนวางบนหน้าจอนี้เพื่อป้อนโดยตรง \n\t\t3.3 QR Code: กระเป๋าเงินสร้าง QR Code ที่คุณสามารถอ่านได้จากปุ่ม QR code ในหน้าจอนี้</string> <string name="create">สร้าง</string> <string name="d">วัน</string> <string name="danger_zone">เขตอันตราย</string> diff --git a/commonsUI/src/commonMain/composeResources/values-th/strings.xml b/commonsUI/src/commonMain/composeResources/values-th/strings.xml index 60a0ddb5c5..a4d606f00b 100644 --- a/commonsUI/src/commonMain/composeResources/values-th/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-th/strings.xml @@ -144,7 +144,7 @@ <string name="send_payment_min_onchain">การ zap แบบ on-chain ต้องใช้อย่างน้อย %1$s sats</string> <string name="send_payment_cashu_insufficient">ยอดคงเหลือใน mint ที่ผู้รับยอมรับไม่เพียงพอ เติมเงินในกระเป๋า Cashu ก่อน</string> <string name="send_payment_onchain_fee">ค่าธรรมเนียมเครือข่าย</string> - <string name="lightning_tips">ส่งทิปให้เบา ๆ </string> + <string name="lightning_tips">ส่งทิปให้เบา ๆ</string> <string name="note_to_receiver">โน้ตถึงผู้รับ</string> <string name="thank_you_so_much">ขอบคุณมาก!</string> <string name="amount_in_sats">จำนวนในหน่วย sat</string> @@ -257,14 +257,8 @@ <string name="mark_all_known_as_read">ทำเครื่องหมายอ่านแล้วในแชทที่รู้จักทั้งหมด</string> <string name="mark_all_new_as_read">ทำเครื่องหมายว่าอ่านแล้วทั้งหมดสำหรับข้อความใหม่</string> <string name="mark_all_as_read">ทพเครื่องหมายว่าอ่านแล้วทั้งหมด</string> - <string name="account_backup_tips2_md"> ## เคล็ดลับการสำรองข้อมูลและความปลอดภัยที่สำคัญ - \n\nบัญชีของคุณได้รับการรักษาความปลอดภัยด้วยคีย์ลับ คีย์เป็นรหัสชุดตัวอักษรยาวขึ้นต้นด้วย **nsec1** ใครก็ตามที่สามารถเข้าถึงรหัสชุดนี้ได้ จะสามารถโพสต์และเปลี่ยนแปลงข้อมูลประจำตัวของคุณได้ - - \n\n- **ห้าม** ใส่รหัสชุดนี้ในเว็บไซต์หรือซอฟต์แวร์ที่คุณไม่ไว้ใจ - \n- ทีมพัฒนา Amethyst **จะไม่มีวัน** ขอรหัสชุดนี้ของคุณ - \n- **ควร** สำรองข้อมูลรหัสชุดนี้ไว้เพื่อกู้คืนบัญชีของคุณ เราแนะนำให้ใช้โปรแกรมจัดการรหัสผ่าน</string> - <string name="account_backup_tips3_md"> สำหรับการรักษาความปลอดภัยเพิ่มเติม คุณสามารถเข้ารหัสคีย์ของคุณด้วยรหัสผ่าน คีย์นี้ขึ้นต้นด้วย **ncryptsec1** และไม่สามารถใช้งานได้หากไม่มีรหัสผ่านของคุณ - \n\n**แต่** หากคุณสูญหายรหัสผ่าน คุณจะไม่สามารถกู้คืนคีย์ของคุณได้ </string> + <string name="account_backup_tips2_md">## เคล็ดลับการสำรองข้อมูลและความปลอดภัยที่สำคัญ \n\nบัญชีของคุณได้รับการรักษาความปลอดภัยด้วยคีย์ลับ คีย์เป็นรหัสชุดตัวอักษรยาวขึ้นต้นด้วย **nsec1** ใครก็ตามที่สามารถเข้าถึงรหัสชุดนี้ได้ จะสามารถโพสต์และเปลี่ยนแปลงข้อมูลประจำตัวของคุณได้ \n\n- **ห้าม** ใส่รหัสชุดนี้ในเว็บไซต์หรือซอฟต์แวร์ที่คุณไม่ไว้ใจ \n- ทีมพัฒนา Amethyst **จะไม่มีวัน** ขอรหัสชุดนี้ของคุณ \n- **ควร** สำรองข้อมูลรหัสชุดนี้ไว้เพื่อกู้คืนบัญชีของคุณ เราแนะนำให้ใช้โปรแกรมจัดการรหัสผ่าน</string> + <string name="account_backup_tips3_md">สำหรับการรักษาความปลอดภัยเพิ่มเติม คุณสามารถเข้ารหัสคีย์ของคุณด้วยรหัสผ่าน คีย์นี้ขึ้นต้นด้วย **ncryptsec1** และไม่สามารถใช้งานได้หากไม่มีรหัสผ่านของคุณ \n\n**แต่** หากคุณสูญหายรหัสผ่าน คุณจะไม่สามารถกู้คืนคีย์ของคุณได้</string> <string name="encrypt_and_copy_my_secret_key">เข้ารหัสและคัดลอกคีย์ความปลอดภัยของฉัน</string> <string name="badge_award_image_for">ภาพเหรียญตราสําหรับ %1$s</string> <string name="badge_award_image">"รูปภาพรางวัลป้าย</string> @@ -1883,7 +1877,7 @@ <string name="back">ย้อนกลับ</string> <string name="backup_keys">สำรองข้อมูลรหัสลับ</string> <string name="badges">แบดจ์</string> - <string name="biometric_authentication_failed"> เกิดข้อผิดพลาดในการตรวจสอบ</string> + <string name="biometric_authentication_failed">เกิดข้อผิดพลาดในการตรวจสอบ</string> <string name="biometric_authentication_failed_explainer">เกิดข้อผิดพลาดจากการยืนยันตนด้วยชีวภาพ</string> <string name="biometric_authentication_failed_explainer_with_error">เกิดข้อผิดพลาดจากการยืนยันตนด้วยชีวภาพ: Error: %1$s</string> <string name="block_only">บล๊อก</string> @@ -1950,7 +1944,7 @@ <string name="cashu">โทเคน Cashu</string> <string name="cashu_checking_mint">กำลังตรวจสอบ mint…</string> <string name="cashu_failed_redemption">ไม่สามารถใช้งาน Cashu</string> - <string name="cashu_failed_redemption_explainer_error_msg">ผู้ให้บริการ Mint เกิดข้อผิดพลาด : %1$s </string> + <string name="cashu_failed_redemption_explainer_error_msg">ผู้ให้บริการ Mint เกิดข้อผิดพลาด : %1$s</string> <string name="cashu_history_incoming">รับแล้ว</string> <string name="cashu_history_outgoing">ส่งแล้ว</string> <string name="cashu_mint_matches_header">Mints ที่ตรงกัน</string> @@ -2042,15 +2036,7 @@ <string name="could_not_fetch_invoice_from_details">ไม่สามารถดึงใบแจ้งหนี้จาก %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">ไม่สามารถเปิดดูข้อมูลที่ถูกบีบอัดได้</string> <string name="couldnt_find_nwc_wallets">ไม่พบกระเป๋าเงิน</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst ไม่พบแอปกระเป๋าเงินที่รองรับ Nostr Wallet Connect (NWC) -\n\nสิ่งที่คุณทำได้: -\n\t1. ตรวจสอบการรองรับ NWC: ตรวจสอบอีกครั้งว่าแอปกระเป๋าเงินที่คุณต้องการใช้รองรับ Nostr Wallet Connect หรือไม่ -\n\t2. สร้างการเชื่อมต่อ: ไปที่แอปกระเป๋าเงินของคุณและสร้างการเชื่อมต่อ NWC ใหม่ -\n\t3. เชื่อมกับ Amethyst: กระเป๋าเงินของคุณมักจะให้ตัวเลือกสามแบบ: -\n\t\t3.1 ลิงก์อัตโนมัติ: ปุ่มเพื่อเปิดและเชื่อมต่อกับ Amethyst โดยอัตโนมัติ หากมี ให้กดปุ่มนั้น เปิด Amethyst และกดบันทึกที่หน้าจอใหม่ -\n\t\t3.2 ลิงก์ด้วยตนเอง: URI การเชื่อมต่อที่คุณสามารถคัดลอกและกดไอคอนวางบนหน้าจอนี้เพื่อป้อนโดยตรง -\n\t\t3.3 QR Code: กระเป๋าเงินสร้าง QR Code ที่คุณสามารถอ่านได้จากปุ่ม QR code ในหน้าจอนี้ - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst ไม่พบแอปกระเป๋าเงินที่รองรับ Nostr Wallet Connect (NWC) \n\nสิ่งที่คุณทำได้: \n\t1. ตรวจสอบการรองรับ NWC: ตรวจสอบอีกครั้งว่าแอปกระเป๋าเงินที่คุณต้องการใช้รองรับ Nostr Wallet Connect หรือไม่ \n\t2. สร้างการเชื่อมต่อ: ไปที่แอปกระเป๋าเงินของคุณและสร้างการเชื่อมต่อ NWC ใหม่ \n\t3. เชื่อมกับ Amethyst: กระเป๋าเงินของคุณมักจะให้ตัวเลือกสามแบบ: \n\t\t3.1 ลิงก์อัตโนมัติ: ปุ่มเพื่อเปิดและเชื่อมต่อกับ Amethyst โดยอัตโนมัติ หากมี ให้กดปุ่มนั้น เปิด Amethyst และกดบันทึกที่หน้าจอใหม่ \n\t\t3.2 ลิงก์ด้วยตนเอง: URI การเชื่อมต่อที่คุณสามารถคัดลอกและกดไอคอนวางบนหน้าจอนี้เพื่อป้อนโดยตรง \n\t\t3.3 QR Code: กระเป๋าเงินสร้าง QR Code ที่คุณสามารถอ่านได้จากปุ่ม QR code ในหน้าจอนี้</string> <string name="create">สร้าง</string> <string name="d">วัน</string> <string name="danger_zone">เขตอันตราย</string> diff --git a/commonsUI/src/commonMain/composeResources/values-tr-rTR/strings.xml b/commonsUI/src/commonMain/composeResources/values-tr-rTR/strings.xml index ac074b789c..59fd97ae80 100644 --- a/commonsUI/src/commonMain/composeResources/values-tr-rTR/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-tr-rTR/strings.xml @@ -71,7 +71,7 @@ <string name="scan_qr">QR Tara</string> <string name="show_anyway">Yine De Göster</string> <string name="post_was_hidden">Bu gönderi, gizlediğiniz kullanıcıları veya kelimeleri içerdiği için gizlendi</string> - <string name="post_was_flagged_as_inappropriate_by">Gönderi uygun değil olarak işaretlendi tarafından: </string> + <string name="post_was_flagged_as_inappropriate_by">Gönderi uygun değil olarak işaretlendi tarafından:</string> <string name="post_not_found">Gönderi bulunamadı</string> <string name="channel_image">Kanal Resmi</string> <string name="referenced_event_not_found">Referanslı etkinlik bulunamadı</string> @@ -272,15 +272,8 @@ <string name="mark_all_known_as_read">Tüm bilinenleri okundu olarak işaretle</string> <string name="mark_all_new_as_read">Tüm yenileri okundu olarak işaretle</string> <string name="mark_all_as_read">Tümünü okundu olarak işaretle</string> - <string name="account_backup_tips2_md"> ## Anahtar Yedekleme ve Güvenlik İpuçları - \n\nHesabınız bir gizli anahtarla korunmaktadır. Anahtar, **nsec1** ile başlayan uzun bir karakter dizisidir. Bu gizli anahtara erişimi olan herkes kimliğiniz adına gönderi yapabilir ve kimliğinizi değiştirebilir. - \n\n- Gizli anahtarınızı güvenmediğiniz herhangi bir web sitesine veya yazılıma **koymayın**. - \n- Amethyst geliştiricileri gizli anahtarınızı **asla** istemez. - \n- Hesap kurtarma için gizli anahtarınızın güvenli bir yedeğini **alın**. Bir parola yöneticisi kullanmanızı öneririz. - </string> - <string name="account_backup_tips3_md"> Ek güvenlik için anahtarınızı bir parola ile şifreleyebilirsiniz. Bu anahtar **ncryptsec1** ile başlar ve parolanız olmadan kullanılamaz. - \n\nParolanızı kaybederseniz anahtarınızı kurtaramazsınız. - </string> + <string name="account_backup_tips2_md">## Anahtar Yedekleme ve Güvenlik İpuçları \n\nHesabınız bir gizli anahtarla korunmaktadır. Anahtar, **nsec1** ile başlayan uzun bir karakter dizisidir. Bu gizli anahtara erişimi olan herkes kimliğiniz adına gönderi yapabilir ve kimliğinizi değiştirebilir. \n\n- Gizli anahtarınızı güvenmediğiniz herhangi bir web sitesine veya yazılıma **koymayın**. \n- Amethyst geliştiricileri gizli anahtarınızı **asla** istemez. \n- Hesap kurtarma için gizli anahtarınızın güvenli bir yedeğini **alın**. Bir parola yöneticisi kullanmanızı öneririz.</string> + <string name="account_backup_tips3_md">Ek güvenlik için anahtarınızı bir parola ile şifreleyebilirsiniz. Bu anahtar **ncryptsec1** ile başlar ve parolanız olmadan kullanılamaz. \n\nParolanızı kaybederseniz anahtarınızı kurtaramazsınız.</string> <string name="encrypt_and_copy_my_secret_key">Gizli anahtarımı şifrele ve kopyala</string> <string name="badge_award_image_for">%1$s için rozet ödülü görseli</string> <string name="badge_award_image">"Rozet ödülü görseli</string> @@ -966,9 +959,7 @@ <string name="push_server_none_explainer">Push Bildirimlerini Devre Dışı Bırakır</string> <string name="push_server_uses_app_explainer">%1$s uygulamasını kullanır</string> <string name="push_server_install_app">Push Bildirim Kurulumu</string> - <string name="push_server_install_app_description"> Push bildirimleri almak için [Ntfy](https://ntfy.sh/) gibi [Unified Push](https://unifiedpush.org/) destekleyen herhangi bir uygulama yükleyin. - Yükledikten sonra, Ayarlar'da kullanmak istediğiniz uygulamayı seçin. - </string> + <string name="push_server_install_app_description">Push bildirimleri almak için [Ntfy](https://ntfy.sh/) gibi [Unified Push](https://unifiedpush.org/) destekleyen herhangi bir uygulama yükleyin. Yükledikten sonra, Ayarlar'da kullanmak istediğiniz uygulamayı seçin.</string> <string name="thread_title">Konu</string> <string name="send_the_seller_a_message">Satıcıya mesaj gönderin</string> <string name="hi_there_is_this_still_available">Merhaba, bu hâlâ mevcut mu?</string> @@ -2024,15 +2015,7 @@ <string name="could_not_fetch_invoice_from_details">%1$s'den fatura alınamadı: %2$s</string> <string name="could_not_open_the_compressed_file">Sıkıştırılmış dosya açılamadı</string> <string name="couldnt_find_nwc_wallets">Cüzdan bulunamadı</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst, Nostr Wallet Connect (NWC) destekleyen herhangi bir cüzdan uygulaması bulamadı. -\n\nYapabilecekleriniz: -\n\t1. NWC Desteğini Doğrulayın: Tercih ettiğiniz cüzdan uygulamasının Nostr Wallet Connect ile uyumlu olup olmadığını iki kez kontrol edin. -\n\t2. Bağlantı Oluşturun: Cüzdan uygulamanıza gidin ve yeni bir NWC bağlantısı oluşturun. -\n\t3. Amethyst'e Bağlayın: Cüzdanınız genellikle size üç seçenek sunar: -\n\t\t3.1 Otomatik Bağlantı: Amethyst'i otomatik olarak açıp bağlamak için bir düğme. Öyleyse o düğmeye tıklayın, Amethyst'i açın ve yeni ekranda kaydet'e basın. -\n\t\t3.2 Manuel Bağlantı: Doğrudan girmek için bu ekrandaki yapıştır simgesine tıklayabileceğiniz bir Bağlantı URI'si kopyalayın. -\n\t\t3.3 QR Kodu: Cüzdan, bu ekrandaki QR kodu düğmesiyle okuyabileceğiniz bir QR kodu oluşturur. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst, Nostr Wallet Connect (NWC) destekleyen herhangi bir cüzdan uygulaması bulamadı. \n\nYapabilecekleriniz: \n\t1. NWC Desteğini Doğrulayın: Tercih ettiğiniz cüzdan uygulamasının Nostr Wallet Connect ile uyumlu olup olmadığını iki kez kontrol edin. \n\t2. Bağlantı Oluşturun: Cüzdan uygulamanıza gidin ve yeni bir NWC bağlantısı oluşturun. \n\t3. Amethyst'e Bağlayın: Cüzdanınız genellikle size üç seçenek sunar: \n\t\t3.1 Otomatik Bağlantı: Amethyst'i otomatik olarak açıp bağlamak için bir düğme. Öyleyse o düğmeye tıklayın, Amethyst'i açın ve yeni ekranda kaydet'e basın. \n\t\t3.2 Manuel Bağlantı: Doğrudan girmek için bu ekrandaki yapıştır simgesine tıklayabileceğiniz bir Bağlantı URI'si kopyalayın. \n\t\t3.3 QR Kodu: Cüzdan, bu ekrandaki QR kodu düğmesiyle okuyabileceğiniz bir QR kodu oluşturur.</string> <string name="create">Oluştur</string> <string name="d">g</string> <string name="danger_zone">Tehlike Bölgesi</string> @@ -2677,11 +2660,8 @@ <string name="proxy_section">Proxy Relay'leri</string> <string name="proxy_section_explainer">Uygulamanın akışlarınızı indirmek için kullanması gereken filter.nostr.wine gibi toplayıcı relay'ler. Bu, giden kutusu modelinin yerini alır ve uygulamanın yalnızca listelerinizdeki relay'lere bağlanmasını sağlar.</string> <string name="public_chat">Herkese Açık Chat</string> - <string name="public_chat_explainer">Genel sohbetler Nostr'daki herkese görünürdür ve herkes - katılabilir. Belirli konular etrafında açık topluluklar için idealdirler. - Moderasyon, relay'lerindeki gönderilerin silinmesiyle kontrol edilebilir</string> - <string name="public_chat_relays_explainer">Bu grubu barındıran 1–3 relay ekleyin. - Nostr istemcileri mesajları nereden indireceğini ve mesajlarınızı nereye göndereceğini öğrenmek için bu ayarı kullanır.</string> + <string name="public_chat_explainer">Genel sohbetler Nostr'daki herkese görünürdür ve herkes katılabilir. Belirli konular etrafında açık topluluklar için idealdirler. Moderasyon, relay'lerindeki gönderilerin silinmesiyle kontrol edilebilir</string> + <string name="public_chat_relays_explainer">Bu grubu barındıran 1–3 relay ekleyin. Nostr istemcileri mesajları nereden indireceğini ve mesajlarınızı nereye göndereceğini öğrenmek için bu ayarı kullanır.</string> <string name="public_chat_relays_title">relay'ler</string> <string name="public_chat_title">Genel Sohbet Meta Verisi</string> <string name="public_chats">Genel Sohbetler</string> @@ -2706,10 +2686,7 @@ <string name="read_only_user">Salt okunur kullanıcı</string> <string name="record_a_message_description">Sesli mesaj kaydetmek için tıklayıp basılı tutun</string> <string name="relay_chat">relay Sohbeti</string> - <string name="relay_chat_explainer">relay sohbetleri, ana relay'leri tarafından kontrol edilen sohbet gruplarıdır. - Nostr'daki herkese görünürdür ve herkes katılabilir. - Belirli konular etrafında açık topluluklar için idealdirler. Bu grupların bir kısmı geçici niteliktedir - ve bu nedenle sohbet mesajları zamanla silinir</string> + <string name="relay_chat_explainer">relay sohbetleri, ana relay'leri tarafından kontrol edilen sohbet gruplarıdır. Nostr'daki herkese görünürdür ve herkes katılabilir. Belirli konular etrafında açık topluluklar için idealdirler. Bu grupların bir kısmı geçici niteliktedir ve bu nedenle sohbet mesajları zamanla silinir</string> <string name="relay_chat_title">relay Sohbetleri</string> <string name="relay_management_tab_events">Etkinlikler</string> <string name="relay_management_tab_ips">IP'ler</string> diff --git a/commonsUI/src/commonMain/composeResources/values-tr/strings.xml b/commonsUI/src/commonMain/composeResources/values-tr/strings.xml index 989465fdb4..7716fa9b4b 100644 --- a/commonsUI/src/commonMain/composeResources/values-tr/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-tr/strings.xml @@ -51,7 +51,7 @@ <string name="scan_qr">QR Tara</string> <string name="show_anyway">Yine De Göster</string> <string name="post_was_hidden">Bu gönderi, gizlediğiniz kullanıcıları veya kelimeleri içerdiği için gizlendi</string> - <string name="post_was_flagged_as_inappropriate_by">Gönderi uygun değil olarak işaretlendi tarafından: </string> + <string name="post_was_flagged_as_inappropriate_by">Gönderi uygun değil olarak işaretlendi tarafından:</string> <string name="post_not_found">Gönderi bulunamadı</string> <string name="post_not_found_short">👀</string> <string name="channel_image">Kanal Resmi</string> @@ -259,17 +259,8 @@ <string name="mark_all_known_as_read">Tüm bilinenleri okundu olarak işaretle</string> <string name="mark_all_new_as_read">Tüm yenileri okundu olarak işaretle</string> <string name="mark_all_as_read">Tümünü okundu olarak işaretle</string> - <string name="account_backup_tips2_md"> - ## Anahtar Yedekleme ve Güvenlik İpuçları - \n\nHesabınız bir gizli anahtarla korunmaktadır. Anahtar, **nsec1** ile başlayan uzun bir karakter dizisidir. Bu gizli anahtara erişimi olan herkes kimliğiniz adına gönderi yapabilir ve kimliğinizi değiştirebilir. - \n\n- Gizli anahtarınızı güvenmediğiniz herhangi bir web sitesine veya yazılıma **koymayın**. - \n- Amethyst geliştiricileri gizli anahtarınızı **asla** istemez. - \n- Hesap kurtarma için gizli anahtarınızın güvenli bir yedeğini **alın**. Bir parola yöneticisi kullanmanızı öneririz. - </string> - <string name="account_backup_tips3_md"> - Ek güvenlik için anahtarınızı bir parola ile şifreleyebilirsiniz. Bu anahtar **ncryptsec1** ile başlar ve parolanız olmadan kullanılamaz. - \n\nParolanızı kaybederseniz anahtarınızı kurtaramazsınız. - </string> + <string name="account_backup_tips2_md">## Anahtar Yedekleme ve Güvenlik İpuçları \n\nHesabınız bir gizli anahtarla korunmaktadır. Anahtar, **nsec1** ile başlayan uzun bir karakter dizisidir. Bu gizli anahtara erişimi olan herkes kimliğiniz adına gönderi yapabilir ve kimliğinizi değiştirebilir. \n\n- Gizli anahtarınızı güvenmediğiniz herhangi bir web sitesine veya yazılıma **koymayın**. \n- Amethyst geliştiricileri gizli anahtarınızı **asla** istemez. \n- Hesap kurtarma için gizli anahtarınızın güvenli bir yedeğini **alın**. Bir parola yöneticisi kullanmanızı öneririz.</string> + <string name="account_backup_tips3_md">Ek güvenlik için anahtarınızı bir parola ile şifreleyebilirsiniz. Bu anahtar **ncryptsec1** ile başlar ve parolanız olmadan kullanılamaz. \n\nParolanızı kaybederseniz anahtarınızı kurtaramazsınız.</string> <string name="encrypt_and_copy_my_secret_key">Gizli anahtarımı şifrele ve kopyala</string> <string name="badge_award_image_for">%1$s için rozet ödülü görseli</string> <string name="badge_award_image">"Rozet ödülü görseli</string> @@ -962,10 +953,7 @@ <string name="push_server_none_explainer">Push Bildirimlerini Devre Dışı Bırakır</string> <string name="push_server_uses_app_explainer">%1$s uygulamasını kullanır</string> <string name="push_server_install_app">Push Bildirim Kurulumu</string> - <string name="push_server_install_app_description"> - Push bildirimleri almak için [Ntfy](https://ntfy.sh/) gibi [Unified Push](https://unifiedpush.org/) destekleyen herhangi bir uygulama yükleyin. - Yükledikten sonra, Ayarlar'da kullanmak istediğiniz uygulamayı seçin. - </string> + <string name="push_server_install_app_description">Push bildirimleri almak için [Ntfy](https://ntfy.sh/) gibi [Unified Push](https://unifiedpush.org/) destekleyen herhangi bir uygulama yükleyin. Yükledikten sonra, Ayarlar'da kullanmak istediğiniz uygulamayı seçin.</string> <string name="thread_title">Konu</string> <string name="send_the_seller_a_message">Satıcıya mesaj gönderin</string> <string name="hi_there_is_this_still_available">Merhaba, bu hâlâ mevcut mu?</string> @@ -2058,15 +2046,7 @@ <string name="could_not_fetch_invoice_from_details">%1$s'den fatura alınamadı: %2$s</string> <string name="could_not_open_the_compressed_file">Sıkıştırılmış dosya açılamadı</string> <string name="couldnt_find_nwc_wallets">Cüzdan bulunamadı</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst, Nostr Wallet Connect (NWC) destekleyen herhangi bir cüzdan uygulaması bulamadı. -\n\nYapabilecekleriniz: -\n\t1. NWC Desteğini Doğrulayın: Tercih ettiğiniz cüzdan uygulamasının Nostr Wallet Connect ile uyumlu olup olmadığını iki kez kontrol edin. -\n\t2. Bağlantı Oluşturun: Cüzdan uygulamanıza gidin ve yeni bir NWC bağlantısı oluşturun. -\n\t3. Amethyst'e Bağlayın: Cüzdanınız genellikle size üç seçenek sunar: -\n\t\t3.1 Otomatik Bağlantı: Amethyst'i otomatik olarak açıp bağlamak için bir düğme. Öyleyse o düğmeye tıklayın, Amethyst'i açın ve yeni ekranda kaydet'e basın. -\n\t\t3.2 Manuel Bağlantı: Doğrudan girmek için bu ekrandaki yapıştır simgesine tıklayabileceğiniz bir Bağlantı URI'si kopyalayın. -\n\t\t3.3 QR Kodu: Cüzdan, bu ekrandaki QR kodu düğmesiyle okuyabileceğiniz bir QR kodu oluşturur. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst, Nostr Wallet Connect (NWC) destekleyen herhangi bir cüzdan uygulaması bulamadı. \n\nYapabilecekleriniz: \n\t1. NWC Desteğini Doğrulayın: Tercih ettiğiniz cüzdan uygulamasının Nostr Wallet Connect ile uyumlu olup olmadığını iki kez kontrol edin. \n\t2. Bağlantı Oluşturun: Cüzdan uygulamanıza gidin ve yeni bir NWC bağlantısı oluşturun. \n\t3. Amethyst'e Bağlayın: Cüzdanınız genellikle size üç seçenek sunar: \n\t\t3.1 Otomatik Bağlantı: Amethyst'i otomatik olarak açıp bağlamak için bir düğme. Öyleyse o düğmeye tıklayın, Amethyst'i açın ve yeni ekranda kaydet'e basın. \n\t\t3.2 Manuel Bağlantı: Doğrudan girmek için bu ekrandaki yapıştır simgesine tıklayabileceğiniz bir Bağlantı URI'si kopyalayın. \n\t\t3.3 QR Kodu: Cüzdan, bu ekrandaki QR kodu düğmesiyle okuyabileceğiniz bir QR kodu oluşturur.</string> <string name="create">Oluştur</string> <string name="d">g</string> <string name="danger_zone">Tehlike Bölgesi</string> @@ -2754,11 +2734,8 @@ <string name="proxy_section">Proxy Relay'leri</string> <string name="proxy_section_explainer">Uygulamanın akışlarınızı indirmek için kullanması gereken filter.nostr.wine gibi toplayıcı relay'ler. Bu, giden kutusu modelinin yerini alır ve uygulamanın yalnızca listelerinizdeki relay'lere bağlanmasını sağlar.</string> <string name="public_chat">Herkese Açık Chat</string> - <string name="public_chat_explainer">Genel sohbetler Nostr'daki herkese görünürdür ve herkes - katılabilir. Belirli konular etrafında açık topluluklar için idealdirler. - Moderasyon, relay'lerindeki gönderilerin silinmesiyle kontrol edilebilir</string> - <string name="public_chat_relays_explainer">Bu grubu barındıran 1–3 relay ekleyin. - Nostr istemcileri mesajları nereden indireceğini ve mesajlarınızı nereye göndereceğini öğrenmek için bu ayarı kullanır.</string> + <string name="public_chat_explainer">Genel sohbetler Nostr'daki herkese görünürdür ve herkes katılabilir. Belirli konular etrafında açık topluluklar için idealdirler. Moderasyon, relay'lerindeki gönderilerin silinmesiyle kontrol edilebilir</string> + <string name="public_chat_relays_explainer">Bu grubu barındıran 1–3 relay ekleyin. Nostr istemcileri mesajları nereden indireceğini ve mesajlarınızı nereye göndereceğini öğrenmek için bu ayarı kullanır.</string> <string name="public_chat_relays_title">relay'ler</string> <string name="public_chat_title">Genel Sohbet Meta Verisi</string> <string name="public_chats">Genel Sohbetler</string> @@ -2783,10 +2760,7 @@ <string name="read_only_user">Salt okunur kullanıcı</string> <string name="record_a_message_description">Sesli mesaj kaydetmek için tıklayıp basılı tutun</string> <string name="relay_chat">relay Sohbeti</string> - <string name="relay_chat_explainer">relay sohbetleri, ana relay'leri tarafından kontrol edilen sohbet gruplarıdır. - Nostr'daki herkese görünürdür ve herkes katılabilir. - Belirli konular etrafında açık topluluklar için idealdirler. Bu grupların bir kısmı geçici niteliktedir - ve bu nedenle sohbet mesajları zamanla silinir</string> + <string name="relay_chat_explainer">relay sohbetleri, ana relay'leri tarafından kontrol edilen sohbet gruplarıdır. Nostr'daki herkese görünürdür ve herkes katılabilir. Belirli konular etrafında açık topluluklar için idealdirler. Bu grupların bir kısmı geçici niteliktedir ve bu nedenle sohbet mesajları zamanla silinir</string> <string name="relay_chat_title">relay Sohbetleri</string> <string name="relay_management_tab_events">Etkinlikler</string> <string name="relay_management_tab_ips">IP'ler</string> diff --git a/commonsUI/src/commonMain/composeResources/values-uk-rUA/strings.xml b/commonsUI/src/commonMain/composeResources/values-uk-rUA/strings.xml index a6e1bee6c5..5e60c8e1da 100644 --- a/commonsUI/src/commonMain/composeResources/values-uk-rUA/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-uk-rUA/strings.xml @@ -225,7 +225,7 @@ <string name="copy_user_id">Скопіювати ID користувача</string> <string name="unblock_user">Розблокувати користувача</string> <string name="user_is_blocked_hidden">Цього користувача заблоковано/заглушено. Їхні публікації приховано.</string> - <string name="npub_hex_username">npub, hex, нікнейм </string> + <string name="npub_hex_username">npub, hex, нікнейм</string> <string name="clear">Очистити</string> <string name="app_logo">Логотип додатка</string> <string name="nsec_npub_hex_private_key">nsec / npub / приватний ключ в hex</string> @@ -273,15 +273,8 @@ <string name="mark_all_known_as_read">Позначити всі відомі прочитаними</string> <string name="mark_all_new_as_read">Позначити всі нові прочитаними</string> <string name="mark_all_as_read">Позначити все прочитаним</string> - <string name="account_backup_tips2_md"> ## Поради щодо резервного копіювання та безпеки ключа - \n\nВаш обліковий запис захищений секретним ключем. Ключ — це довга послідовність символів, що починається з **nsec1**. Будь-хто, хто має доступ до цього секретного ключа, може публікувати та змінювати вашу особу. - \n\n- **Не** вводьте свій секретний ключ на жодному сайті чи в програмі, якій ви не довіряєте. - \n- Розробники Amethyst **ніколи** не запитуватимуть ваш секретний ключ. - \n- **Зробіть** надійне резервне копіювання секретного ключа для відновлення облікового запису. Рекомендуємо використовувати менеджер паролів. - </string> - <string name="account_backup_tips3_md"> Для додаткової безпеки ви можете зашифрувати ключ паролем. Цей ключ починається з **ncryptsec1** і не може бути використаний без вашого пароля. - \n\nЯкщо ви забудете пароль, ви не зможете відновити ключ. - </string> + <string name="account_backup_tips2_md">## Поради щодо резервного копіювання та безпеки ключа \n\nВаш обліковий запис захищений секретним ключем. Ключ — це довга послідовність символів, що починається з **nsec1**. Будь-хто, хто має доступ до цього секретного ключа, може публікувати та змінювати вашу особу. \n\n- **Не** вводьте свій секретний ключ на жодному сайті чи в програмі, якій ви не довіряєте. \n- Розробники Amethyst **ніколи** не запитуватимуть ваш секретний ключ. \n- **Зробіть** надійне резервне копіювання секретного ключа для відновлення облікового запису. Рекомендуємо використовувати менеджер паролів.</string> + <string name="account_backup_tips3_md">Для додаткової безпеки ви можете зашифрувати ключ паролем. Цей ключ починається з **ncryptsec1** і не може бути використаний без вашого пароля. \n\nЯкщо ви забудете пароль, ви не зможете відновити ключ.</string> <string name="encrypt_and_copy_my_secret_key">Зашифрувати та скопіювати мій секретний ключ</string> <string name="badge_award_image_for">Картинка значка-нагороди %1$s</string> <string name="badge_award_image">"Зображення нагороди значком</string> @@ -970,9 +963,7 @@ <string name="push_server_none_explainer">Вимикає push-сповіщення</string> <string name="push_server_uses_app_explainer">Використовує додаток %1$s</string> <string name="push_server_install_app">Налаштування push-сповіщень</string> - <string name="push_server_install_app_description"> Щоб отримувати push-сповіщення, встановіть будь-який додаток з підтримкою [Unified Push](https://unifiedpush.org/), наприклад [Ntfy](https://ntfy.sh/). - Після встановлення виберіть потрібний додаток у налаштуваннях. - </string> + <string name="push_server_install_app_description">Щоб отримувати push-сповіщення, встановіть будь-який додаток з підтримкою [Unified Push](https://unifiedpush.org/), наприклад [Ntfy](https://ntfy.sh/). Після встановлення виберіть потрібний додаток у налаштуваннях.</string> <string name="thread_title">Тема</string> <string name="send_the_seller_a_message">Надіслати продавцю повідомлення</string> <string name="hi_there_is_this_still_available">Привіт, це ще доступно?</string> @@ -2037,15 +2028,7 @@ <string name="could_not_fetch_invoice_from_details">Не вдалося отримати інвойс від %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Не вдалося відкрити стиснутий файл</string> <string name="couldnt_find_nwc_wallets">Гаманець не знайдено</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst не знайшов жодного гаманця, який підтримує Nostr Wallet Connect (NWC). -\n\nЩо можна зробити: -\n\t1. Перевірте підтримку NWC: переконайтеся, що ваш гаманець сумісний з Nostr Wallet Connect. -\n\t2. Створіть підключення: відкрийте гаманець і створіть нове NWC-підключення. -\n\t3. Прив'яжіть до Amethyst: зазвичай гаманець надає три варіанти: -\n\t\t3.1 Автоматичне підключення: кнопка для автоматичного відкриття та підключення до Amethyst. Якщо так — натисніть її, відкрийте Amethyst і збережіть налаштування на новому екрані. -\n\t\t3.2 Ручне підключення: URI підключення, який можна скопіювати та вставити за допомогою значка вставки на цьому екрані. -\n\t\t3.3 QR-код: гаманець створює QR-код, який можна зчитати за допомогою кнопки QR-коду на цьому екрані. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst не знайшов жодного гаманця, який підтримує Nostr Wallet Connect (NWC). \n\nЩо можна зробити: \n\t1. Перевірте підтримку NWC: переконайтеся, що ваш гаманець сумісний з Nostr Wallet Connect. \n\t2. Створіть підключення: відкрийте гаманець і створіть нове NWC-підключення. \n\t3. Прив'яжіть до Amethyst: зазвичай гаманець надає три варіанти: \n\t\t3.1 Автоматичне підключення: кнопка для автоматичного відкриття та підключення до Amethyst. Якщо так — натисніть її, відкрийте Amethyst і збережіть налаштування на новому екрані. \n\t\t3.2 Ручне підключення: URI підключення, який можна скопіювати та вставити за допомогою значка вставки на цьому екрані. \n\t\t3.3 QR-код: гаманець створює QR-код, який можна зчитати за допомогою кнопки QR-коду на цьому екрані.</string> <string name="create">Створити</string> <string name="d">дн</string> <string name="danger_zone">Небезпечна зона</string> @@ -2726,11 +2709,8 @@ <string name="proxy_section">Proxy relay-сервери</string> <string name="proxy_section_explainer">Агрегаторні relay-сервери, які застосунок має використовувати для завантаження ваших стрічок, наприклад filter.nostr.wine. Це замінює модель outbox і змусить застосунок підключатися лише до relay-серверів у ваших списках.</string> <string name="public_chat">Публічний чат</string> - <string name="public_chat_explainer">Публічні чати видимі всім у Nostr, і кожен - може брати в них участь. Вони чудово підходять для відкритих спільнот за певними темами. - Модерацію можна здійснювати, видаляючи публікації на відповідних relay</string> - <string name="public_chat_relays_explainer">Вкажіть від 1 до 3 relay, що розміщують цю групу. - Клієнти Nostr використовують це налаштування, щоб знати, звідки завантажувати повідомлення та куди їх надсилати.</string> + <string name="public_chat_explainer">Публічні чати видимі всім у Nostr, і кожен може брати в них участь. Вони чудово підходять для відкритих спільнот за певними темами. Модерацію можна здійснювати, видаляючи публікації на відповідних relay</string> + <string name="public_chat_relays_explainer">Вкажіть від 1 до 3 relay, що розміщують цю групу. Клієнти Nostr використовують це налаштування, щоб знати, звідки завантажувати повідомлення та куди їх надсилати.</string> <string name="public_chat_relays_title">Relay</string> <string name="public_chat_title">Метадані публічного чату</string> <string name="public_chats">Публічні чати</string> @@ -2755,10 +2735,7 @@ <string name="read_only_user">Користувач лише для читання</string> <string name="record_a_message_description">Натисніть і утримуйте, щоб записати повідомлення</string> <string name="relay_chat">Чат relay</string> - <string name="relay_chat_explainer">Чати relay — це групи чатів, керовані їхнім домашнім relay. - Вони видимі всім у Nostr, і кожен може брати в них участь. - Вони чудово підходять для відкритих спільнот за певними темами. Деякі з цих груп є тимчасовими, - і тому повідомлення чату зникають з часом</string> + <string name="relay_chat_explainer">Чати relay — це групи чатів, керовані їхнім домашнім relay. Вони видимі всім у Nostr, і кожен може брати в них участь. Вони чудово підходять для відкритих спільнот за певними темами. Деякі з цих груп є тимчасовими, і тому повідомлення чату зникають з часом</string> <string name="relay_chat_title">Чати relay</string> <string name="relay_management_tab_events">Події</string> <string name="relay_management_tab_ips">IP-адреси</string> diff --git a/commonsUI/src/commonMain/composeResources/values-uk/strings.xml b/commonsUI/src/commonMain/composeResources/values-uk/strings.xml index 53159e447e..bc7b9935b3 100644 --- a/commonsUI/src/commonMain/composeResources/values-uk/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-uk/strings.xml @@ -215,7 +215,7 @@ <string name="copy_user_id">Скопіювати ID користувача</string> <string name="unblock_user">Розблокувати користувача</string> <string name="user_is_blocked_hidden">Цього користувача заблоковано/заглушено. Їхні публікації приховано.</string> - <string name="npub_hex_username">npub, hex, нікнейм </string> + <string name="npub_hex_username">npub, hex, нікнейм</string> <string name="clear">Очистити</string> <string name="app_logo">Логотип додатка</string> <string name="nsec_npub_hex_private_key">nsec / npub / приватний ключ в hex</string> @@ -263,17 +263,8 @@ <string name="mark_all_known_as_read">Позначити всі відомі прочитаними</string> <string name="mark_all_new_as_read">Позначити всі нові прочитаними</string> <string name="mark_all_as_read">Позначити все прочитаним</string> - <string name="account_backup_tips2_md"> - ## Поради щодо резервного копіювання та безпеки ключа - \n\nВаш обліковий запис захищений секретним ключем. Ключ — це довга послідовність символів, що починається з **nsec1**. Будь-хто, хто має доступ до цього секретного ключа, може публікувати та змінювати вашу особу. - \n\n- **Не** вводьте свій секретний ключ на жодному сайті чи в програмі, якій ви не довіряєте. - \n- Розробники Amethyst **ніколи** не запитуватимуть ваш секретний ключ. - \n- **Зробіть** надійне резервне копіювання секретного ключа для відновлення облікового запису. Рекомендуємо використовувати менеджер паролів. - </string> - <string name="account_backup_tips3_md"> - Для додаткової безпеки ви можете зашифрувати ключ паролем. Цей ключ починається з **ncryptsec1** і не може бути використаний без вашого пароля. - \n\nЯкщо ви забудете пароль, ви не зможете відновити ключ. - </string> + <string name="account_backup_tips2_md">## Поради щодо резервного копіювання та безпеки ключа \n\nВаш обліковий запис захищений секретним ключем. Ключ — це довга послідовність символів, що починається з **nsec1**. Будь-хто, хто має доступ до цього секретного ключа, може публікувати та змінювати вашу особу. \n\n- **Не** вводьте свій секретний ключ на жодному сайті чи в програмі, якій ви не довіряєте. \n- Розробники Amethyst **ніколи** не запитуватимуть ваш секретний ключ. \n- **Зробіть** надійне резервне копіювання секретного ключа для відновлення облікового запису. Рекомендуємо використовувати менеджер паролів.</string> + <string name="account_backup_tips3_md">Для додаткової безпеки ви можете зашифрувати ключ паролем. Цей ключ починається з **ncryptsec1** і не може бути використаний без вашого пароля. \n\nЯкщо ви забудете пароль, ви не зможете відновити ключ.</string> <string name="encrypt_and_copy_my_secret_key">Зашифрувати та скопіювати мій секретний ключ</string> <string name="badge_award_image_for">Картинка значка-нагороди %1$s</string> <string name="badge_award_image">"Зображення нагороди значком</string> @@ -966,10 +957,7 @@ <string name="push_server_none_explainer">Вимикає push-сповіщення</string> <string name="push_server_uses_app_explainer">Використовує додаток %1$s</string> <string name="push_server_install_app">Налаштування push-сповіщень</string> - <string name="push_server_install_app_description"> - Щоб отримувати push-сповіщення, встановіть будь-який додаток з підтримкою [Unified Push](https://unifiedpush.org/), наприклад [Ntfy](https://ntfy.sh/). - Після встановлення виберіть потрібний додаток у налаштуваннях. - </string> + <string name="push_server_install_app_description">Щоб отримувати push-сповіщення, встановіть будь-який додаток з підтримкою [Unified Push](https://unifiedpush.org/), наприклад [Ntfy](https://ntfy.sh/). Після встановлення виберіть потрібний додаток у налаштуваннях.</string> <string name="thread_title">Тема</string> <string name="send_the_seller_a_message">Надіслати продавцю повідомлення</string> <string name="hi_there_is_this_still_available">Привіт, це ще доступно?</string> @@ -2078,15 +2066,7 @@ <string name="could_not_fetch_invoice_from_details">Не вдалося отримати інвойс від %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Не вдалося відкрити стиснутий файл</string> <string name="couldnt_find_nwc_wallets">Гаманець не знайдено</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst не знайшов жодного гаманця, який підтримує Nostr Wallet Connect (NWC). -\n\nЩо можна зробити: -\n\t1. Перевірте підтримку NWC: переконайтеся, що ваш гаманець сумісний з Nostr Wallet Connect. -\n\t2. Створіть підключення: відкрийте гаманець і створіть нове NWC-підключення. -\n\t3. Прив'яжіть до Amethyst: зазвичай гаманець надає три варіанти: -\n\t\t3.1 Автоматичне підключення: кнопка для автоматичного відкриття та підключення до Amethyst. Якщо так — натисніть її, відкрийте Amethyst і збережіть налаштування на новому екрані. -\n\t\t3.2 Ручне підключення: URI підключення, який можна скопіювати та вставити за допомогою значка вставки на цьому екрані. -\n\t\t3.3 QR-код: гаманець створює QR-код, який можна зчитати за допомогою кнопки QR-коду на цьому екрані. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst не знайшов жодного гаманця, який підтримує Nostr Wallet Connect (NWC). \n\nЩо можна зробити: \n\t1. Перевірте підтримку NWC: переконайтеся, що ваш гаманець сумісний з Nostr Wallet Connect. \n\t2. Створіть підключення: відкрийте гаманець і створіть нове NWC-підключення. \n\t3. Прив'яжіть до Amethyst: зазвичай гаманець надає три варіанти: \n\t\t3.1 Автоматичне підключення: кнопка для автоматичного відкриття та підключення до Amethyst. Якщо так — натисніть її, відкрийте Amethyst і збережіть налаштування на новому екрані. \n\t\t3.2 Ручне підключення: URI підключення, який можна скопіювати та вставити за допомогою значка вставки на цьому екрані. \n\t\t3.3 QR-код: гаманець створює QR-код, який можна зчитати за допомогою кнопки QR-коду на цьому екрані.</string> <string name="create">Створити</string> <string name="d">дн</string> <string name="danger_zone">Небезпечна зона</string> @@ -2816,11 +2796,8 @@ <string name="proxy_section">Proxy relay-сервери</string> <string name="proxy_section_explainer">Агрегаторні relay-сервери, які застосунок має використовувати для завантаження ваших стрічок, наприклад filter.nostr.wine. Це замінює модель outbox і змусить застосунок підключатися лише до relay-серверів у ваших списках.</string> <string name="public_chat">Публічний чат</string> - <string name="public_chat_explainer">Публічні чати видимі всім у Nostr, і кожен - може брати в них участь. Вони чудово підходять для відкритих спільнот за певними темами. - Модерацію можна здійснювати, видаляючи публікації на відповідних relay</string> - <string name="public_chat_relays_explainer">Вкажіть від 1 до 3 relay, що розміщують цю групу. - Клієнти Nostr використовують це налаштування, щоб знати, звідки завантажувати повідомлення та куди їх надсилати.</string> + <string name="public_chat_explainer">Публічні чати видимі всім у Nostr, і кожен може брати в них участь. Вони чудово підходять для відкритих спільнот за певними темами. Модерацію можна здійснювати, видаляючи публікації на відповідних relay</string> + <string name="public_chat_relays_explainer">Вкажіть від 1 до 3 relay, що розміщують цю групу. Клієнти Nostr використовують це налаштування, щоб знати, звідки завантажувати повідомлення та куди їх надсилати.</string> <string name="public_chat_relays_title">Relay</string> <string name="public_chat_title">Метадані публічного чату</string> <string name="public_chats">Публічні чати</string> @@ -2845,10 +2822,7 @@ <string name="read_only_user">Користувач лише для читання</string> <string name="record_a_message_description">Натисніть і утримуйте, щоб записати повідомлення</string> <string name="relay_chat">Чат relay</string> - <string name="relay_chat_explainer">Чати relay — це групи чатів, керовані їхнім домашнім relay. - Вони видимі всім у Nostr, і кожен може брати в них участь. - Вони чудово підходять для відкритих спільнот за певними темами. Деякі з цих груп є тимчасовими, - і тому повідомлення чату зникають з часом</string> + <string name="relay_chat_explainer">Чати relay — це групи чатів, керовані їхнім домашнім relay. Вони видимі всім у Nostr, і кожен може брати в них участь. Вони чудово підходять для відкритих спільнот за певними темами. Деякі з цих груп є тимчасовими, і тому повідомлення чату зникають з часом</string> <string name="relay_chat_title">Чати relay</string> <string name="relay_management_tab_events">Події</string> <string name="relay_management_tab_ips">IP-адреси</string> diff --git a/commonsUI/src/commonMain/composeResources/values-uz-rUZ/strings.xml b/commonsUI/src/commonMain/composeResources/values-uz-rUZ/strings.xml index 332e2bae8f..3c8e9daa2a 100644 --- a/commonsUI/src/commonMain/composeResources/values-uz-rUZ/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-uz-rUZ/strings.xml @@ -273,15 +273,8 @@ <string name="mark_all_known_as_read">Barcha ma'lumlarni o'qilgan deb belgilash</string> <string name="mark_all_new_as_read">Barcha yangilarni o'qilgan deb belgilash</string> <string name="mark_all_as_read">Barchasini o'qilgan deb belgilash</string> - <string name="account_backup_tips2_md"> ## Kalit zaxiralash va xavfsizlik maslahatlari - \n\nHisobingiz maxfiy kalit bilan himoyalangan. Kalit **nsec1** bilan boshlanadigan uzun belgilar ketma-ketligidir. Ushbu maxfiy kalitga kirish huquqiga ega bo'lgan har kim sizning nomingizdan post qo'yishi va identifikatoringizni o'zgartirishi mumkin. - \n\n- Maxfiy kalitingizni ishonmagan veb-sayt yoki dasturga **kiritmang**. - \n- Amethyst ishlab chiquvchilari maxfiy kalitingizni hech **qachon** so'ramaydi. - \n- Hisob tiklash uchun maxfiy kalitingizning xavfsiz zaxira nusxasini saqlang. Parol menejeridan foydalanishni tavsiya qilamiz. - </string> - <string name="account_backup_tips3_md"> Qo'shimcha xavfsizlik uchun kalitingizni parol bilan shifrlashingiz mumkin. Bu kalit **ncryptsec1** bilan boshlanadi va parolsiz ishlatib bo'lmaydi. - \n\nParolingizni unutsangiz, kalitingizni tiklay olmaysiz. - </string> + <string name="account_backup_tips2_md">## Kalit zaxiralash va xavfsizlik maslahatlari \n\nHisobingiz maxfiy kalit bilan himoyalangan. Kalit **nsec1** bilan boshlanadigan uzun belgilar ketma-ketligidir. Ushbu maxfiy kalitga kirish huquqiga ega bo'lgan har kim sizning nomingizdan post qo'yishi va identifikatoringizni o'zgartirishi mumkin. \n\n- Maxfiy kalitingizni ishonmagan veb-sayt yoki dasturga **kiritmang**. \n- Amethyst ishlab chiquvchilari maxfiy kalitingizni hech **qachon** so'ramaydi. \n- Hisob tiklash uchun maxfiy kalitingizning xavfsiz zaxira nusxasini saqlang. Parol menejeridan foydalanishni tavsiya qilamiz.</string> + <string name="account_backup_tips3_md">Qo'shimcha xavfsizlik uchun kalitingizni parol bilan shifrlashingiz mumkin. Bu kalit **ncryptsec1** bilan boshlanadi va parolsiz ishlatib bo'lmaydi. \n\nParolingizni unutsangiz, kalitingizni tiklay olmaysiz.</string> <string name="encrypt_and_copy_my_secret_key">Maxfiy kalitni shifrlash va nusxalash</string> <string name="badge_award_image_for">%1$s uchun nishon mukofoti rasmi</string> <string name="badge_award_image">"Nishon mukofoti rasmi</string> @@ -962,9 +955,7 @@ <string name="push_server_none_explainer">Push bildirishnomalarini o'chiradi</string> <string name="push_server_uses_app_explainer">%1$s ilovasidan foydalanadi</string> <string name="push_server_install_app">Push bildirishnoma sozlamasi</string> - <string name="push_server_install_app_description"> Push bildirishnomalarni olish uchun [Ntfy](https://ntfy.sh/) kabi [Unified Push](https://unifiedpush.org/) ni qo'llab-quvvatlaydigan ilovani o'rnating. - O'rnatgandan so'ng, Sozlamalarda foydalanmoqchi bo'lgan ilovani tanlang. - </string> + <string name="push_server_install_app_description">Push bildirishnomalarni olish uchun [Ntfy](https://ntfy.sh/) kabi [Unified Push](https://unifiedpush.org/) ni qo'llab-quvvatlaydigan ilovani o'rnating. O'rnatgandan so'ng, Sozlamalarda foydalanmoqchi bo'lgan ilovani tanlang.</string> <string name="thread_title">Ip</string> <string name="send_the_seller_a_message">Sotuvchiga xabar yuborish</string> <string name="hi_there_is_this_still_available">Salom, bu hali ham mavjudmi?</string> @@ -2012,15 +2003,7 @@ <string name="could_not_fetch_invoice_from_details">%1$s dan hisob-faktura olishda xato: %2$s</string> <string name="could_not_open_the_compressed_file">Siqilgan faylni ochib bo'lmadi</string> <string name="couldnt_find_nwc_wallets">Hamyon topilmadi</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst Nostr Wallet Connect (NWC) ni qo'llab-quvvatlaydigan hech qanday hamyon ilovasini topa olmadi. -\n\nNima qilishingiz mumkin: -\n\t1. NWC qo'llab-quvvatlashini tekshirish: Tanlagan hamyon ilovangiz Nostr Wallet Connect bilan mos kelishini ikki marta tekshiring. -\n\t2. Ulanish yaratish: Hamyon ilovangizga o'ting va yangi NWC ulanishini yarating. -\n\t3. Amethyst ga bog'lash: Hamyoningiz odatda uchta variant taqdim etadi: -\n\t\t3.1 Avtomatik bog'lash: Amethyst ni avtomatik ochish va ulash tugmasi. Agar shunday bo'lsa, ushbu tugmani bosing, Amethyst ni oching va yangi ekranda saqlash tugmasini bosing. -\n\t\t3.2 Qo'lda bog'lash: Nusxa olishingiz va to'g'ridan-to'g'ri kiritish uchun ushbu ekrandagi joylashtirish belgisini bosishingiz mumkin bo'lgan Ulanish URI si. -\n\t\t3.3 QR kod: Hamyon ushbu ekrandagi QR kod tugmasidan o'qishingiz mumkin bo'lgan QR kod yaratadi. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst Nostr Wallet Connect (NWC) ni qo'llab-quvvatlaydigan hech qanday hamyon ilovasini topa olmadi. \n\nNima qilishingiz mumkin: \n\t1. NWC qo'llab-quvvatlashini tekshirish: Tanlagan hamyon ilovangiz Nostr Wallet Connect bilan mos kelishini ikki marta tekshiring. \n\t2. Ulanish yaratish: Hamyon ilovangizga o'ting va yangi NWC ulanishini yarating. \n\t3. Amethyst ga bog'lash: Hamyoningiz odatda uchta variant taqdim etadi: \n\t\t3.1 Avtomatik bog'lash: Amethyst ni avtomatik ochish va ulash tugmasi. Agar shunday bo'lsa, ushbu tugmani bosing, Amethyst ni oching va yangi ekranda saqlash tugmasini bosing. \n\t\t3.2 Qo'lda bog'lash: Nusxa olishingiz va to'g'ridan-to'g'ri kiritish uchun ushbu ekrandagi joylashtirish belgisini bosishingiz mumkin bo'lgan Ulanish URI si. \n\t\t3.3 QR kod: Hamyon ushbu ekrandagi QR kod tugmasidan o'qishingiz mumkin bo'lgan QR kod yaratadi.</string> <string name="create">Yaratish</string> <string name="d">k</string> <string name="danger_zone">Xavfli zona</string> @@ -2686,11 +2669,8 @@ <string name="proxy_section">Proksi Relay lar</string> <string name="proxy_section_explainer">Ilovaning tasmalangizni yuklab olish uchun foydalanishi kerak bo'lgan agregator relay lar, masalan filter.nostr.wine. Bu chiqib ketish modelini almashtiradi va ilovani faqat ro'yxatlaringizdagi relay larga ulanishga majbur qiladi.</string> <string name="public_chat">Ommaviy chat</string> - <string name="public_chat_explainer">Ommaviy chatlar Nostr dagi hamma uchun ko'rinadi va har kim - ularda ishtirok eta oladi. Ular muayyan mavzular bo'yicha ochiq jamoalar uchun juda mos keladi. - Moderatsiya relay'larida postlarni o'chirish orqali boshqarilishi mumkin</string> - <string name="public_chat_relays_explainer">Ushbu guruhni joylashtiradigan 1-3 ta relay kiriting. - Nostr klientlari xabarlarni qayerdan yuklab olishini va xabarlaringizni qayerga yuborishini bilish uchun ushbu sozlamadan foydalanadi.</string> + <string name="public_chat_explainer">Ommaviy chatlar Nostr dagi hamma uchun ko'rinadi va har kim ularda ishtirok eta oladi. Ular muayyan mavzular bo'yicha ochiq jamoalar uchun juda mos keladi. Moderatsiya relay'larida postlarni o'chirish orqali boshqarilishi mumkin</string> + <string name="public_chat_relays_explainer">Ushbu guruhni joylashtiradigan 1-3 ta relay kiriting. Nostr klientlari xabarlarni qayerdan yuklab olishini va xabarlaringizni qayerga yuborishini bilish uchun ushbu sozlamadan foydalanadi.</string> <string name="public_chat_relays_title">Relay lar</string> <string name="public_chat_title">Ommaviy chat metama'lumotlari</string> <string name="public_chats">Ommaviy chatlar</string> @@ -2715,10 +2695,7 @@ <string name="read_only_user">Faqat o'qish foydalanuvchisi</string> <string name="record_a_message_description">Xabar yozib olish uchun bosib ushlab turing</string> <string name="relay_chat">Relay chati</string> - <string name="relay_chat_explainer">Relay chatlari o'z uy relay'lari tomonidan boshqariladigan chat guruhlaridir. - Ular Nostr dagi hamma uchun ko'rinadi va har kim ularda ishtirok eta oladi. - Ular muayyan mavzular bo'yicha ochiq jamoalar uchun juda mos keladi. Bu guruhlarning bir qismi vaqtinchalik bo'lib, - shuning uchun chat xabarlari vaqt o'tishi bilan yo'qoladi</string> + <string name="relay_chat_explainer">Relay chatlari o'z uy relay'lari tomonidan boshqariladigan chat guruhlaridir. Ular Nostr dagi hamma uchun ko'rinadi va har kim ularda ishtirok eta oladi. Ular muayyan mavzular bo'yicha ochiq jamoalar uchun juda mos keladi. Bu guruhlarning bir qismi vaqtinchalik bo'lib, shuning uchun chat xabarlari vaqt o'tishi bilan yo'qoladi</string> <string name="relay_chat_title">Relay chatlari</string> <string name="relay_management_tab_events">Hodisalar</string> <string name="relay_management_tab_ips">IP lar</string> diff --git a/commonsUI/src/commonMain/composeResources/values-vi-rVN/strings.xml b/commonsUI/src/commonMain/composeResources/values-vi-rVN/strings.xml index a875b48f0e..e39e556680 100644 --- a/commonsUI/src/commonMain/composeResources/values-vi-rVN/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-vi-rVN/strings.xml @@ -267,15 +267,8 @@ <string name="mark_all_known_as_read">Đánh dấu tất cả đã biết là đã đọc</string> <string name="mark_all_new_as_read">Đánh dấu tất cả mới là đã đọc</string> <string name="mark_all_as_read">Đánh dấu tất cả là đã đọc</string> - <string name="account_backup_tips2_md"> ## Mẹo sao lưu khóa và bảo mật - \n\nTài khoản của bạn được bảo mật bằng một khóa bí mật. Khóa là một chuỗi ký tự dài bắt đầu bằng **nsec1**. Bất kỳ ai có quyền truy cập vào khóa bí mật này đều có thể đăng bài và thay đổi danh tính của bạn. - \n\n- **Không** đưa khóa bí mật của bạn vào bất kỳ trang web hoặc phần mềm nào mà bạn không tin tưởng. - \n- Các nhà phát triển Amethyst sẽ **không bao giờ** yêu cầu khóa bí mật của bạn. - \n- **Hãy** giữ bản sao lưu an toàn của khóa bí mật để khôi phục tài khoản. Chúng tôi khuyến nghị sử dụng trình quản lý mật khẩu. - </string> - <string name="account_backup_tips3_md"> Để bảo mật thêm, bạn có thể mã hóa khóa của mình bằng mật khẩu. Khóa này bắt đầu bằng **ncryptsec1** và không thể sử dụng nếu không có mật khẩu của bạn. - \n\nNếu bạn mất mật khẩu, bạn sẽ không thể khôi phục khóa của mình. - </string> + <string name="account_backup_tips2_md">## Mẹo sao lưu khóa và bảo mật \n\nTài khoản của bạn được bảo mật bằng một khóa bí mật. Khóa là một chuỗi ký tự dài bắt đầu bằng **nsec1**. Bất kỳ ai có quyền truy cập vào khóa bí mật này đều có thể đăng bài và thay đổi danh tính của bạn. \n\n- **Không** đưa khóa bí mật của bạn vào bất kỳ trang web hoặc phần mềm nào mà bạn không tin tưởng. \n- Các nhà phát triển Amethyst sẽ **không bao giờ** yêu cầu khóa bí mật của bạn. \n- **Hãy** giữ bản sao lưu an toàn của khóa bí mật để khôi phục tài khoản. Chúng tôi khuyến nghị sử dụng trình quản lý mật khẩu.</string> + <string name="account_backup_tips3_md">Để bảo mật thêm, bạn có thể mã hóa khóa của mình bằng mật khẩu. Khóa này bắt đầu bằng **ncryptsec1** và không thể sử dụng nếu không có mật khẩu của bạn. \n\nNếu bạn mất mật khẩu, bạn sẽ không thể khôi phục khóa của mình.</string> <string name="encrypt_and_copy_my_secret_key">Mã hóa và sao chép khóa bí mật của tôi</string> <string name="badge_award_image_for">Hình ảnh huy hiệu được trao cho %1$s</string> <string name="badge_award_image">"Hình ảnh huy hiệu được trao</string> @@ -954,9 +947,7 @@ <string name="push_server_none_explainer">Tắt thông báo Push</string> <string name="push_server_uses_app_explainer">Sử dụng ứng dụng %1$s</string> <string name="push_server_install_app">Cài đặt thông báo Push</string> - <string name="push_server_install_app_description"> Để nhận thông báo push, hãy cài đặt ứng dụng hỗ trợ [Unified Push](https://unifiedpush.org/), chẳng hạn như [Ntfy](https://ntfy.sh/). - Sau khi cài đặt, hãy chọn ứng dụng bạn muốn sử dụng trong Cài đặt. - </string> + <string name="push_server_install_app_description">Để nhận thông báo push, hãy cài đặt ứng dụng hỗ trợ [Unified Push](https://unifiedpush.org/), chẳng hạn như [Ntfy](https://ntfy.sh/). Sau khi cài đặt, hãy chọn ứng dụng bạn muốn sử dụng trong Cài đặt.</string> <string name="thread_title">Luồng</string> <string name="send_the_seller_a_message">Gửi tin nhắn cho người bán</string> <string name="hi_there_is_this_still_available">Xin chào, mặt hàng này còn không?</string> @@ -2000,15 +1991,7 @@ <string name="could_not_fetch_invoice_from_details">Không thể tải hóa đơn từ %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Không thể mở tệp nén</string> <string name="couldnt_find_nwc_wallets">Không tìm thấy ví</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst không tìm thấy ứng dụng ví nào hỗ trợ Nostr Wallet Connect (NWC). -\n\nBạn có thể thực hiện: -\n\t1. Xác minh hỗ trợ NWC: Kiểm tra lại xem ứng dụng ví của bạn có tương thích với Nostr Wallet Connect không. -\n\t2. Tạo kết nối: Vào ứng dụng ví của bạn và tạo kết nối NWC mới. -\n\t3. Liên kết với Amethyst: Ví của bạn thường cung cấp ba tùy chọn: -\n\t\t3.1 Liên kết tự động: Nút để tự động mở và kết nối với Amethyst. Nếu có, nhấn nút đó, mở Amethyst và nhấn lưu trên màn hình mới. -\n\t\t3.2 Liên kết thủ công: URI kết nối mà bạn có thể sao chép và nhấn biểu tượng dán trên màn hình này để nhập trực tiếp. -\n\t\t3.3 Mã QR: Ví tạo mã QR mà bạn có thể đọc bằng nút mã QR trên màn hình này. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst không tìm thấy ứng dụng ví nào hỗ trợ Nostr Wallet Connect (NWC). \n\nBạn có thể thực hiện: \n\t1. Xác minh hỗ trợ NWC: Kiểm tra lại xem ứng dụng ví của bạn có tương thích với Nostr Wallet Connect không. \n\t2. Tạo kết nối: Vào ứng dụng ví của bạn và tạo kết nối NWC mới. \n\t3. Liên kết với Amethyst: Ví của bạn thường cung cấp ba tùy chọn: \n\t\t3.1 Liên kết tự động: Nút để tự động mở và kết nối với Amethyst. Nếu có, nhấn nút đó, mở Amethyst và nhấn lưu trên màn hình mới. \n\t\t3.2 Liên kết thủ công: URI kết nối mà bạn có thể sao chép và nhấn biểu tượng dán trên màn hình này để nhập trực tiếp. \n\t\t3.3 Mã QR: Ví tạo mã QR mà bạn có thể đọc bằng nút mã QR trên màn hình này.</string> <string name="create">Tạo</string> <string name="d">n</string> <string name="danger_zone">Vùng nguy hiểm</string> @@ -2642,11 +2625,8 @@ <string name="proxy_section">Relay Proxy</string> <string name="proxy_section_explainer">Các relay tổng hợp mà ứng dụng phải sử dụng để tải feed của bạn, như filter.nostr.wine. Tùy chọn này thay thế mô hình outbox và sẽ khiến ứng dụng chỉ kết nối đến các relay trong danh sách của bạn.</string> <string name="public_chat">Trò chuyện công khai</string> - <string name="public_chat_explainer">Trò chuyện công khai hiển thị với mọi người trên Nostr và bất kỳ ai - cũng có thể tham gia. Chúng rất phù hợp cho các cộng đồng mở xung quanh các chủ đề cụ thể. - Kiểm duyệt có thể được kiểm soát bằng cách xóa bài đăng trên các relay của nó</string> - <string name="public_chat_relays_explainer">Nhập từ 1–3 relay lưu trữ nhóm này. - Các ứng dụng Nostr sử dụng cài đặt này để biết nơi tải xuống tin nhắn và gửi tin nhắn của bạn.</string> + <string name="public_chat_explainer">Trò chuyện công khai hiển thị với mọi người trên Nostr và bất kỳ ai cũng có thể tham gia. Chúng rất phù hợp cho các cộng đồng mở xung quanh các chủ đề cụ thể. Kiểm duyệt có thể được kiểm soát bằng cách xóa bài đăng trên các relay của nó</string> + <string name="public_chat_relays_explainer">Nhập từ 1–3 relay lưu trữ nhóm này. Các ứng dụng Nostr sử dụng cài đặt này để biết nơi tải xuống tin nhắn và gửi tin nhắn của bạn.</string> <string name="public_chat_relays_title">Relay</string> <string name="public_chat_title">Siêu dữ liệu trò chuyện công khai</string> <string name="public_chats">Trò chuyện công khai</string> @@ -2670,10 +2650,7 @@ <string name="read_from_relay_description">Lượng dữ liệu tính bằng byte đã nhận được từ relay này, bao gồm các bộ lọc và sự kiện</string> <string name="read_only_user">Người dùng chỉ đọc</string> <string name="record_a_message_description">Nhấn và giữ để ghi âm tin nhắn</string> - <string name="relay_chat_explainer">Relay chat là các nhóm trò chuyện được kiểm soát bởi relay chủ quản. - Chúng hiển thị với mọi người trên Nostr và bất kỳ ai cũng có thể tham gia. - Chúng rất phù hợp cho các cộng đồng mở xung quanh các chủ đề cụ thể. Một số nhóm này là tạm thời - và do đó tin nhắn trò chuyện sẽ biến mất theo thời gian</string> + <string name="relay_chat_explainer">Relay chat là các nhóm trò chuyện được kiểm soát bởi relay chủ quản. Chúng hiển thị với mọi người trên Nostr và bất kỳ ai cũng có thể tham gia. Chúng rất phù hợp cho các cộng đồng mở xung quanh các chủ đề cụ thể. Một số nhóm này là tạm thời và do đó tin nhắn trò chuyện sẽ biến mất theo thời gian</string> <string name="relay_chat_title">Relay Chat</string> <string name="relay_management_tab_events">Sự kiện</string> <string name="relay_management_tab_ips">IP</string> diff --git a/commonsUI/src/commonMain/composeResources/values-zh-rCN/strings.xml b/commonsUI/src/commonMain/composeResources/values-zh-rCN/strings.xml index d16f98dff4..36527eeeaf 100644 --- a/commonsUI/src/commonMain/composeResources/values-zh-rCN/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-zh-rCN/strings.xml @@ -389,15 +389,8 @@ <string name="mark_all_known_as_read">将所有已知内容标记为已读</string> <string name="mark_all_new_as_read">将所有新内容标记为已读</string> <string name="mark_all_as_read">将所有内容标记为已读</string> - <string name="account_backup_tips2_md">## 备份与安全提示 - \n\n你的帐户由一个私钥保护。私钥是以**nsec1**开头的随机字符串。任何拥有你的私钥的人都可以使用你的身份发布内容。 - \n\n- **不要**将你的私钥添加到任何你不信任的网站或软件,亦不要在网上公开。 - \n- Amethyst 开发人员**永远不会**要求你提供私钥。 - \n- **请**保留你的私钥的安全备份,以备帐户恢复。我们建议使用密码管理器。 - </string> - <string name="account_backup_tips3_md"> 为了额外的安全性,你可以用密码加密你的密钥。 此密钥以 **ncryptsec1** 开头,没有密码就不能使用。 - \n\n如果你丢失密码, 你将无法恢复你的密钥。 - </string> + <string name="account_backup_tips2_md">## 备份与安全提示 \n\n你的帐户由一个私钥保护。私钥是以**nsec1**开头的随机字符串。任何拥有你的私钥的人都可以使用你的身份发布内容。 \n\n- **不要**将你的私钥添加到任何你不信任的网站或软件,亦不要在网上公开。 \n- Amethyst 开发人员**永远不会**要求你提供私钥。 \n- **请**保留你的私钥的安全备份,以备帐户恢复。我们建议使用密码管理器。</string> + <string name="account_backup_tips3_md">为了额外的安全性,你可以用密码加密你的密钥。 此密钥以 **ncryptsec1** 开头,没有密码就不能使用。 \n\n如果你丢失密码, 你将无法恢复你的密钥。</string> <string name="encrypt_and_copy_my_secret_key">加密并复制我的密钥。</string> <string name="badge_award_image_for">颁发给 %1$s 的徽章图片</string> <string name="badge_award_image">"徽章图片</string> @@ -478,7 +471,7 @@ <string name="new_community_rules_kinds_hint">轻触以允许一种类型。长按一种类型来设置每个事件的大小或每日帖子的限制。</string> <string name="new_community_rules_kind_custom_label">自定义类型</string> <string name="new_community_rules_kind_add">添加类型</string> - <string name="new_community_rules_limits_title"> %1$d 类型的限制</string> + <string name="new_community_rules_limits_title">%1$d 类型的限制</string> <string name="new_community_rules_limits_max_bytes">最大事件大小(字节)</string> <string name="new_community_rules_limits_max_per_day">每位作者每天最多发帖数</string> <string name="new_community_rules_limits_unlimited">留空为无限制</string> @@ -1356,9 +1349,7 @@ <string name="push_server_none_explainer">关闭推送通知</string> <string name="push_server_uses_app_explainer">使用应用 %1$s</string> <string name="push_server_install_app">推送通知设置</string> - <string name="push_server_install_app_description"> 若要接收推送通知,请安装任何支持 [Unified Push](https://unifiedpus)的应用, 例如 [Ntfy](https://ntfy.sh/)。 - 安装后,请在设置中选择你想要使用的应用程序。 - </string> + <string name="push_server_install_app_description">若要接收推送通知,请安装任何支持 [Unified Push](https://unifiedpus)的应用, 例如 [Ntfy](https://ntfy.sh/)。 安装后,请在设置中选择你想要使用的应用程序。</string> <string name="thread_title">主题帖</string> <string name="send_the_seller_a_message">向卖方发送消息</string> <string name="hi_there_is_this_still_available">你好,这个还能买到吗?</string> @@ -2640,15 +2631,7 @@ <string name="could_not_fetch_invoice_from_details">无法从 %1$s 获取发票:%2$s</string> <string name="could_not_open_the_compressed_file">无法打开压缩文件</string> <string name="couldnt_find_nwc_wallets">未找到钱包</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst 找不到任何支持 Nostr Wallet Connect (NWC) 的钱包应用. -\n\n你可以做的事情: -\n\t1. 验证 NWC 支持:仔细确认偏好的钱包应用和 Nostr Wallet Connect 兼容。 -\n\t2. 创建连接:前往钱包应用并创建新的 NWC 连接。 -\n\t3.链接到 Amethyst: 你的钱包通常会给你三个选项: -\n\t\t3.1 自动链接:自动打开并连接到 Amethyst 的按钮。如果是这样, 单击那个按钮,打开 Amethyst 并在新屏上按保存。 -\n\t\t3.2 手动链接: 你可以复制的连接 URI,单击这个屏幕上的粘贴图标直接输入它。 -\n\t\t3.3 二维码: 钱包会创建一个你可以从这个屏幕中的二维码按钮那里读取的二维码。 - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst 找不到任何支持 Nostr Wallet Connect (NWC) 的钱包应用. \n\n你可以做的事情: \n\t1. 验证 NWC 支持:仔细确认偏好的钱包应用和 Nostr Wallet Connect 兼容。 \n\t2. 创建连接:前往钱包应用并创建新的 NWC 连接。 \n\t3.链接到 Amethyst: 你的钱包通常会给你三个选项: \n\t\t3.1 自动链接:自动打开并连接到 Amethyst 的按钮。如果是这样, 单击那个按钮,打开 Amethyst 并在新屏上按保存。 \n\t\t3.2 手动链接: 你可以复制的连接 URI,单击这个屏幕上的粘贴图标直接输入它。 \n\t\t3.3 二维码: 钱包会创建一个你可以从这个屏幕中的二维码按钮那里读取的二维码。</string> <string name="create">创建</string> <string name="d">天</string> <string name="danger_zone">危险区</string> @@ -3552,11 +3535,8 @@ <string name="proxy_section">代理中继</string> <string name="proxy_section_explainer">聚合器中继是应用获取资讯源时使用的中继(例如:filter.nostr.wine),设置这个列表将覆盖信箱模型的行为,使得应用只会连接到这个列表中的中继以获取内容。</string> <string name="public_chat">公共聊天</string> - <string name="public_chat_explainer">公开聊天对所有 Nostr 用户都是可见的,任何人都可以: - 参与这些聊天。这对特定话题建立的开放式社区来说很重要 - 通过删除中继上的帖子实现内容审核控制</string> - <string name="public_chat_relays_explainer">使用 1~3 个中继托管这个群组。 - 让 Nostr 客户端知道应该使用这些中继配置来发送和下载消息。</string> + <string name="public_chat_explainer">公开聊天对所有 Nostr 用户都是可见的,任何人都可以: 参与这些聊天。这对特定话题建立的开放式社区来说很重要 通过删除中继上的帖子实现内容审核控制</string> + <string name="public_chat_relays_explainer">使用 1~3 个中继托管这个群组。 让 Nostr 客户端知道应该使用这些中继配置来发送和下载消息。</string> <string name="public_chat_relays_title">中继器</string> <string name="public_chat_title">公开聊天元数据</string> <string name="public_chats">公共聊天</string> @@ -3589,10 +3569,7 @@ <string name="relay_auth_search_keywords">auth authentication relay sign verify nip-42 identity</string> <string name="relay_auth_settings_title">中继身份认证</string> <string name="relay_chat">中继聊天</string> - <string name="relay_chat_explainer">中继聊天是由聊天所属中继控制的聊天群组。 - Nostr 上的每个人都可以看到这些群组,任何人都可以加入群组。 - 这非常适合围绕特定主题而建立的开放社区。其中一些群组是临时的, - 因此聊天信息会随着时间的推移而消失。</string> + <string name="relay_chat_explainer">中继聊天是由聊天所属中继控制的聊天群组。 Nostr 上的每个人都可以看到这些群组,任何人都可以加入群组。 这非常适合围绕特定主题而建立的开放社区。其中一些群组是临时的, 因此聊天信息会随着时间的推移而消失。</string> <string name="relay_chat_title">中继聊天</string> <string name="relay_group_create_title">建群</string> <string name="relay_group_edit_title">编辑群</string> diff --git a/commonsUI/src/commonMain/composeResources/values-zh-rHK/strings.xml b/commonsUI/src/commonMain/composeResources/values-zh-rHK/strings.xml index 9eb6ba5bc8..4ee4682e0a 100644 --- a/commonsUI/src/commonMain/composeResources/values-zh-rHK/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-zh-rHK/strings.xml @@ -228,7 +228,7 @@ <string name="copy_user_id">複製用户ID</string> <string name="unblock_user">隱藏該用户</string> <string name="user_is_blocked_hidden">此用户被屏蔽/静音。他们的帖子被隐藏。</string> - <string name="npub_hex_username">npub,hex,用户名 </string> + <string name="npub_hex_username">npub,hex,用户名</string> <string name="clear">清除</string> <string name="app_logo">應用標誌</string> <string name="nsec_npub_hex_private_key">nsec/ npub/ hex 私鑰</string> @@ -276,15 +276,8 @@ <string name="mark_all_known_as_read">將所有已知內容標記為已讀</string> <string name="mark_all_new_as_read">將所有新內容標記為已讀</string> <string name="mark_all_as_read">將所有內容標記為已讀</string> - <string name="account_backup_tips2_md">## 备份与安全提示 - \n\n你的帐户由一个私钥保护。私钥是以**nsec1**开头的随机字符串。任何拥有你的私钥的人都可以使用你的身份发布内容。 - \n\n- **不要**将你的私钥添加到任何你不信任的网站或软件,亦不要在网上公开。 - \n- Amethyst 开发人员**永远不会**要求你提供私钥。 - \n- **请**保留你的私钥的安全备份,以备帐户恢复。我们建议使用密码管理器。 - </string> - <string name="account_backup_tips3_md"> 为了额外的安全性,你可以用密码加密你的密钥。 此密钥以 **ncryptsec1** 开头,没有密码就不能使用。 - \n\n如果你丢失密码, 你将无法恢复你的密钥。 - </string> + <string name="account_backup_tips2_md">## 备份与安全提示 \n\n你的帐户由一个私钥保护。私钥是以**nsec1**开头的随机字符串。任何拥有你的私钥的人都可以使用你的身份发布内容。 \n\n- **不要**将你的私钥添加到任何你不信任的网站或软件,亦不要在网上公开。 \n- Amethyst 开发人员**永远不会**要求你提供私钥。 \n- **请**保留你的私钥的安全备份,以备帐户恢复。我们建议使用密码管理器。</string> + <string name="account_backup_tips3_md">为了额外的安全性,你可以用密码加密你的密钥。 此密钥以 **ncryptsec1** 开头,没有密码就不能使用。 \n\n如果你丢失密码, 你将无法恢复你的密钥。</string> <string name="encrypt_and_copy_my_secret_key">加密并复制我的密钥。</string> <string name="badge_award_image_for">頒發給 %1$s 的徽章圖片</string> <string name="badge_award_image">"徽章图片</string> @@ -365,7 +358,7 @@ <string name="new_community_rules_kinds_hint">轻触以允许一种类型。长按一种类型来设置每个事件的大小或每日帖子的限制。</string> <string name="new_community_rules_kind_custom_label">自定义类型</string> <string name="new_community_rules_kind_add">添加类型</string> - <string name="new_community_rules_limits_title"> %1$d 类型的限制</string> + <string name="new_community_rules_limits_title">%1$d 类型的限制</string> <string name="new_community_rules_limits_max_bytes">最大事件大小(字节)</string> <string name="new_community_rules_limits_max_per_day">每位作者每天最多发帖数</string> <string name="new_community_rules_limits_unlimited">留空为无限制</string> @@ -976,9 +969,7 @@ <string name="push_server_none_explainer">关闭推送通知</string> <string name="push_server_uses_app_explainer">使用应用 %1$s</string> <string name="push_server_install_app">推送通知设置</string> - <string name="push_server_install_app_description"> 若要接收推送通知,请安装任何支持 [Unified Push](https://unifiedpus)的应用, 例如 [Ntfy](https://ntfy.sh/)。 - 安装后,请在设置中选择你想要使用的应用程序。 - </string> + <string name="push_server_install_app_description">若要接收推送通知,请安装任何支持 [Unified Push](https://unifiedpus)的应用, 例如 [Ntfy](https://ntfy.sh/)。 安装后,请在设置中选择你想要使用的应用程序。</string> <string name="thread_title">主题帖</string> <string name="send_the_seller_a_message">向卖方发送消息</string> <string name="hi_there_is_this_still_available">你好,这个还能买到吗?</string> @@ -2037,15 +2028,7 @@ <string name="could_not_fetch_invoice_from_details">无法从 %1$s 获取发票:%2$s</string> <string name="could_not_open_the_compressed_file">无法打开压缩文件</string> <string name="couldnt_find_nwc_wallets">未找到钱包</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst 找不到任何支持 Nostr Wallet Connect (NWC) 的钱包应用. -\n\n你可以做的事情: -\n\t1. 验证 NWC 支持:仔细确认偏好的钱包应用和 Nostr Wallet Connect 兼容。 -\n\t2. 创建连接:前往钱包应用并创建新的 NWC 连接。 -\n\t3.链接到 Amethyst: 你的钱包通常会给你三个选项: -\n\t\t3.1 自动链接:自动打开并连接到 Amethyst 的按钮。如果是这样, 单击那个按钮,打开 Amethyst 并在新屏上按保存。 -\n\t\t3.2 手动链接: 你可以复制的连接 URI,单击这个屏幕上的粘贴图标直接输入它。 -\n\t\t3.3 二维码: 钱包会创建一个你可以从这个屏幕中的二维码按钮那里读取的二维码。 - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst 找不到任何支持 Nostr Wallet Connect (NWC) 的钱包应用. \n\n你可以做的事情: \n\t1. 验证 NWC 支持:仔细确认偏好的钱包应用和 Nostr Wallet Connect 兼容。 \n\t2. 创建连接:前往钱包应用并创建新的 NWC 连接。 \n\t3.链接到 Amethyst: 你的钱包通常会给你三个选项: \n\t\t3.1 自动链接:自动打开并连接到 Amethyst 的按钮。如果是这样, 单击那个按钮,打开 Amethyst 并在新屏上按保存。 \n\t\t3.2 手动链接: 你可以复制的连接 URI,单击这个屏幕上的粘贴图标直接输入它。 \n\t\t3.3 二维码: 钱包会创建一个你可以从这个屏幕中的二维码按钮那里读取的二维码。</string> <string name="create">創建</string> <string name="d">天</string> <string name="danger_zone">危险区</string> @@ -2700,11 +2683,8 @@ <string name="proxy_section">代理中继</string> <string name="proxy_section_explainer">聚合器中继是应用获取资讯源时使用的中继(例如:filter.nostr.wine),设置这个列表将覆盖信箱模型的行为,使得应用只会连接到这个列表中的中继以获取内容。</string> <string name="public_chat">公共聊天</string> - <string name="public_chat_explainer">公开聊天对所有 Nostr 用户都是可见的,任何人都可以: - 参与这些聊天。这对特定话题建立的开放式社区来说很重要 - 通过删除中继上的帖子实现内容审核控制</string> - <string name="public_chat_relays_explainer">使用 1~3 个中继托管这个群组。 - 让 Nostr 客户端知道应该使用这些中继配置来发送和下载消息。</string> + <string name="public_chat_explainer">公开聊天对所有 Nostr 用户都是可见的,任何人都可以: 参与这些聊天。这对特定话题建立的开放式社区来说很重要 通过删除中继上的帖子实现内容审核控制</string> + <string name="public_chat_relays_explainer">使用 1~3 个中继托管这个群组。 让 Nostr 客户端知道应该使用这些中继配置来发送和下载消息。</string> <string name="public_chat_relays_title">中继器</string> <string name="public_chat_title">公开聊天元数据</string> <string name="public_chats">公共聊天</string> @@ -2729,10 +2709,7 @@ <string name="read_only_user">仅读用户</string> <string name="record_a_message_description">点按以录制消息</string> <string name="relay_chat">中继聊天</string> - <string name="relay_chat_explainer">中继聊天是由聊天所属中继控制的聊天群组。 - Nostr 上的每个人都可以看到这些群组,任何人都可以加入群组。 - 这非常适合围绕特定主题而建立的开放社区。其中一些群组是临时的, - 因此聊天信息会随着时间的推移而消失。</string> + <string name="relay_chat_explainer">中继聊天是由聊天所属中继控制的聊天群组。 Nostr 上的每个人都可以看到这些群组,任何人都可以加入群组。 这非常适合围绕特定主题而建立的开放社区。其中一些群组是临时的, 因此聊天信息会随着时间的推移而消失。</string> <string name="relay_chat_title">中继聊天</string> <string name="relay_management_tab_events">事件</string> <string name="relay_management_tab_ips">IP地址</string> diff --git a/commonsUI/src/commonMain/composeResources/values-zh-rSG/strings.xml b/commonsUI/src/commonMain/composeResources/values-zh-rSG/strings.xml index 4649ff00d5..0d0a9f7f9e 100644 --- a/commonsUI/src/commonMain/composeResources/values-zh-rSG/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-zh-rSG/strings.xml @@ -276,15 +276,8 @@ <string name="mark_all_known_as_read">将所有已知内容标记为已读</string> <string name="mark_all_new_as_read">将所有新内容标记为已读</string> <string name="mark_all_as_read">将所有内容标记为已读</string> - <string name="account_backup_tips2_md">## 备份与安全提示 - \n\n你的帐户由一个私钥保护。私钥是以**nsec1**开头的随机字符串。任何拥有你的私钥的人都可以使用你的身份发布内容。 - \n\n- **不要**将你的私钥添加到任何你不信任的网站或软件,亦不要在网上公开。 - \n- Amethyst 开发人员**永远不会**要求你提供私钥。 - \n- **请**保留你的私钥的安全备份,以备帐户恢复。我们建议使用密码管理器。 - </string> - <string name="account_backup_tips3_md"> 为了额外的安全性,你可以用密码加密你的密钥。 此密钥以 **ncryptsec1** 开头,没有密码就不能使用。 - \n\n如果你丢失密码, 你将无法恢复你的密钥。 - </string> + <string name="account_backup_tips2_md">## 备份与安全提示 \n\n你的帐户由一个私钥保护。私钥是以**nsec1**开头的随机字符串。任何拥有你的私钥的人都可以使用你的身份发布内容。 \n\n- **不要**将你的私钥添加到任何你不信任的网站或软件,亦不要在网上公开。 \n- Amethyst 开发人员**永远不会**要求你提供私钥。 \n- **请**保留你的私钥的安全备份,以备帐户恢复。我们建议使用密码管理器。</string> + <string name="account_backup_tips3_md">为了额外的安全性,你可以用密码加密你的密钥。 此密钥以 **ncryptsec1** 开头,没有密码就不能使用。 \n\n如果你丢失密码, 你将无法恢复你的密钥。</string> <string name="encrypt_and_copy_my_secret_key">加密并复制我的密钥。</string> <string name="badge_award_image_for">颁发给 %1$s 的徽章图片</string> <string name="badge_award_image">"徽章图片</string> @@ -365,7 +358,7 @@ <string name="new_community_rules_kinds_hint">轻触以允许一种类型。长按一种类型来设置每个事件的大小或每日帖子的限制。</string> <string name="new_community_rules_kind_custom_label">自定义类型</string> <string name="new_community_rules_kind_add">添加类型</string> - <string name="new_community_rules_limits_title"> %1$d 类型的限制</string> + <string name="new_community_rules_limits_title">%1$d 类型的限制</string> <string name="new_community_rules_limits_max_bytes">最大事件大小(字节)</string> <string name="new_community_rules_limits_max_per_day">每位作者每天最多发帖数</string> <string name="new_community_rules_limits_unlimited">留空为无限制</string> @@ -976,9 +969,7 @@ <string name="push_server_none_explainer">关闭推送通知</string> <string name="push_server_uses_app_explainer">使用应用 %1$s</string> <string name="push_server_install_app">推送通知设置</string> - <string name="push_server_install_app_description"> 若要接收推送通知,请安装任何支持 [Unified Push](https://unifiedpus)的应用, 例如 [Ntfy](https://ntfy.sh/)。 - 安装后,请在设置中选择你想要使用的应用程序。 - </string> + <string name="push_server_install_app_description">若要接收推送通知,请安装任何支持 [Unified Push](https://unifiedpus)的应用, 例如 [Ntfy](https://ntfy.sh/)。 安装后,请在设置中选择你想要使用的应用程序。</string> <string name="thread_title">主题帖</string> <string name="send_the_seller_a_message">向卖方发送消息</string> <string name="hi_there_is_this_still_available">你好,这个还能买到吗?</string> @@ -2037,15 +2028,7 @@ <string name="could_not_fetch_invoice_from_details">无法从 %1$s 获取发票:%2$s</string> <string name="could_not_open_the_compressed_file">无法打开压缩文件</string> <string name="couldnt_find_nwc_wallets">未找到钱包</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst 找不到任何支持 Nostr Wallet Connect (NWC) 的钱包应用. -\n\n你可以做的事情: -\n\t1. 验证 NWC 支持:仔细确认偏好的钱包应用和 Nostr Wallet Connect 兼容。 -\n\t2. 创建连接:前往钱包应用并创建新的 NWC 连接。 -\n\t3.链接到 Amethyst: 你的钱包通常会给你三个选项: -\n\t\t3.1 自动链接:自动打开并连接到 Amethyst 的按钮。如果是这样, 单击那个按钮,打开 Amethyst 并在新屏上按保存。 -\n\t\t3.2 手动链接: 你可以复制的连接 URI,单击这个屏幕上的粘贴图标直接输入它。 -\n\t\t3.3 二维码: 钱包会创建一个你可以从这个屏幕中的二维码按钮那里读取的二维码。 - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst 找不到任何支持 Nostr Wallet Connect (NWC) 的钱包应用. \n\n你可以做的事情: \n\t1. 验证 NWC 支持:仔细确认偏好的钱包应用和 Nostr Wallet Connect 兼容。 \n\t2. 创建连接:前往钱包应用并创建新的 NWC 连接。 \n\t3.链接到 Amethyst: 你的钱包通常会给你三个选项: \n\t\t3.1 自动链接:自动打开并连接到 Amethyst 的按钮。如果是这样, 单击那个按钮,打开 Amethyst 并在新屏上按保存。 \n\t\t3.2 手动链接: 你可以复制的连接 URI,单击这个屏幕上的粘贴图标直接输入它。 \n\t\t3.3 二维码: 钱包会创建一个你可以从这个屏幕中的二维码按钮那里读取的二维码。</string> <string name="create">创建</string> <string name="d">天</string> <string name="danger_zone">危险区</string> @@ -2700,11 +2683,8 @@ <string name="proxy_section">代理中继</string> <string name="proxy_section_explainer">聚合器中继是应用获取资讯源时使用的中继(例如:filter.nostr.wine),设置这个列表将覆盖信箱模型的行为,使得应用只会连接到这个列表中的中继以获取内容。</string> <string name="public_chat">公共聊天</string> - <string name="public_chat_explainer">公开聊天对所有 Nostr 用户都是可见的,任何人都可以: - 参与这些聊天。这对特定话题建立的开放式社区来说很重要 - 通过删除中继上的帖子实现内容审核控制</string> - <string name="public_chat_relays_explainer">使用 1~3 个中继托管这个群组。 - 让 Nostr 客户端知道应该使用这些中继配置来发送和下载消息。</string> + <string name="public_chat_explainer">公开聊天对所有 Nostr 用户都是可见的,任何人都可以: 参与这些聊天。这对特定话题建立的开放式社区来说很重要 通过删除中继上的帖子实现内容审核控制</string> + <string name="public_chat_relays_explainer">使用 1~3 个中继托管这个群组。 让 Nostr 客户端知道应该使用这些中继配置来发送和下载消息。</string> <string name="public_chat_relays_title">中继器</string> <string name="public_chat_title">公开聊天元数据</string> <string name="public_chats">公共聊天</string> @@ -2729,10 +2709,7 @@ <string name="read_only_user">仅读用户</string> <string name="record_a_message_description">点按以录制消息</string> <string name="relay_chat">中继聊天</string> - <string name="relay_chat_explainer">中继聊天是由聊天所属中继控制的聊天群组。 - Nostr 上的每个人都可以看到这些群组,任何人都可以加入群组。 - 这非常适合围绕特定主题而建立的开放社区。其中一些群组是临时的, - 因此聊天信息会随着时间的推移而消失。</string> + <string name="relay_chat_explainer">中继聊天是由聊天所属中继控制的聊天群组。 Nostr 上的每个人都可以看到这些群组,任何人都可以加入群组。 这非常适合围绕特定主题而建立的开放社区。其中一些群组是临时的, 因此聊天信息会随着时间的推移而消失。</string> <string name="relay_chat_title">中继聊天</string> <string name="relay_management_tab_events">事件</string> <string name="relay_management_tab_ips">IP地址</string> diff --git a/commonsUI/src/commonMain/composeResources/values-zh-rTW/strings.xml b/commonsUI/src/commonMain/composeResources/values-zh-rTW/strings.xml index 6bd229d72c..65c10efd5d 100644 --- a/commonsUI/src/commonMain/composeResources/values-zh-rTW/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-zh-rTW/strings.xml @@ -223,7 +223,7 @@ <string name="copy_user_id">複製用户ID</string> <string name="unblock_user">隱藏該用户</string> <string name="user_is_blocked_hidden">此使用者已被封鎖/靜音。其貼文已隱藏。</string> - <string name="npub_hex_username">npub,hex,用户名 </string> + <string name="npub_hex_username">npub,hex,用户名</string> <string name="clear">清除</string> <string name="app_logo">應用標誌</string> <string name="nsec_npub_hex_private_key">nsec/ npub/ hex 私鑰</string> @@ -271,15 +271,8 @@ <string name="mark_all_known_as_read">將所有已知內容標記為已讀</string> <string name="mark_all_new_as_read">將所有新內容標記為已讀</string> <string name="mark_all_as_read">將所有內容標記為已讀</string> - <string name="account_backup_tips2_md">## 備份與安全提示 - \n\n你的帳戶由一個私鑰保護。私鑰是以 **nsec1** 開頭的隨機字符串。任何擁有你的私鑰的人都可以使用你的身份發佈內容。 - \n\n- **不要**將你的私鑰添加到任何你不信任的網站或軟件,亦不要在網上公開。 - \n- Amethyst 開發人員**永遠不會**要求你提供私鑰。 - \n- **請**保留你的私鑰的安全備份,以備帳戶恢復。我們建議使用密碼管理器。 - </string> - <string name="account_backup_tips3_md"> 爲了額外的安全性,你可以用密碼加密你的密鑰。此密鑰以 **ncryptsec1** 開頭,沒有密碼就不能使用。 - \n\n如果你丟失密碼,你將無法恢復你的密鑰。 - </string> + <string name="account_backup_tips2_md">## 備份與安全提示 \n\n你的帳戶由一個私鑰保護。私鑰是以 **nsec1** 開頭的隨機字符串。任何擁有你的私鑰的人都可以使用你的身份發佈內容。 \n\n- **不要**將你的私鑰添加到任何你不信任的網站或軟件,亦不要在網上公開。 \n- Amethyst 開發人員**永遠不會**要求你提供私鑰。 \n- **請**保留你的私鑰的安全備份,以備帳戶恢復。我們建議使用密碼管理器。</string> + <string name="account_backup_tips3_md">爲了額外的安全性,你可以用密碼加密你的密鑰。此密鑰以 **ncryptsec1** 開頭,沒有密碼就不能使用。 \n\n如果你丟失密碼,你將無法恢復你的密鑰。</string> <string name="encrypt_and_copy_my_secret_key">加密並複製我的密鑰</string> <string name="badge_award_image_for">頒發給 %1$s 的徽章圖片</string> <string name="badge_award_image">"徽章獎勵圖片</string> @@ -968,9 +961,7 @@ <string name="push_server_none_explainer">關閉推送通知</string> <string name="push_server_uses_app_explainer">使用應用 %1$s</string> <string name="push_server_install_app">推送通知設置</string> - <string name="push_server_install_app_description"> 若要接收推送通知,請安裝任何支持 [Unified Push](https://unifiedpush.org/)的應用,例如 [Ntfy](https://ntfy.sh/)。 - 安裝後,請在設備中選擇你想要使用的應用程式。 - </string> + <string name="push_server_install_app_description">若要接收推送通知,請安裝任何支持 [Unified Push](https://unifiedpush.org/)的應用,例如 [Ntfy](https://ntfy.sh/)。 安裝後,請在設備中選擇你想要使用的應用程式。</string> <string name="thread_title">主題帖</string> <string name="send_the_seller_a_message">向賣方發送消息</string> <string name="hi_there_is_this_still_available">你好,這個還能買到嗎?</string> @@ -2022,7 +2013,7 @@ <string name="could_not_fetch_invoice_from_details">無法從 %1$s 取得發票:%2$s</string> <string name="could_not_open_the_compressed_file">無法打開壓縮文件</string> <string name="couldnt_find_nwc_wallets">找不到錢包</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst 找不到任何支援 Nostr Wallet Connect (NWC) 的錢包應用程式。\n\n您可以這樣做:\n\t1. 確認 NWC 支援:請再次確認您偏好的錢包應用程式是否與 Nostr Wallet Connect 相容。\n\t2. 建立連線:前往您的錢包應用程式並建立新的 NWC 連線。\n\t3. 連結至 Amethyst:您的錢包通常會提供三種選項:\n\t\t3.1 自動連結:點擊按鈕自動開啟並連接到 Amethyst。若有此選項,請點擊該按鈕,開啟 Amethyst 並在新畫面上點擊儲存。\n\t\t3.2 手動連結:複製連線 URI,然後點擊此畫面上的貼上圖示直接輸入。\n\t\t3.3 QR 碼:錢包會產生 QR 碼,您可透過此畫面上的 QR 碼按鈕掃描。\n </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst 找不到任何支援 Nostr Wallet Connect (NWC) 的錢包應用程式。\n\n您可以這樣做:\n\t1. 確認 NWC 支援:請再次確認您偏好的錢包應用程式是否與 Nostr Wallet Connect 相容。\n\t2. 建立連線:前往您的錢包應用程式並建立新的 NWC 連線。\n\t3. 連結至 Amethyst:您的錢包通常會提供三種選項:\n\t\t3.1 自動連結:點擊按鈕自動開啟並連接到 Amethyst。若有此選項,請點擊該按鈕,開啟 Amethyst 並在新畫面上點擊儲存。\n\t\t3.2 手動連結:複製連線 URI,然後點擊此畫面上的貼上圖示直接輸入。\n\t\t3.3 QR 碼:錢包會產生 QR 碼,您可透過此畫面上的 QR 碼按鈕掃描。\n</string> <string name="create">創建</string> <string name="d">天</string> <string name="danger_zone">危險區域</string> @@ -2664,11 +2655,8 @@ <string name="proxy_section">代理 Relay</string> <string name="proxy_section_explainer">應用程式必須用來下載您的動態的聚合器 relay,例如 filter.nostr.wine。這將取代發件匣模式,使應用程式僅連線至您清單中的 relay。</string> <string name="public_chat">公共聊天</string> - <string name="public_chat_explainer">公開聊天室對所有 Nostr 使用者可見, - 任何人皆可參與。非常適合圍繞特定主題的開放社群。 - 可透過在其 relay 上刪除貼文來進行內容管理</string> - <string name="public_chat_relays_explainer">請輸入 1–3 個託管此群組的 relay。 - Nostr 客戶端使用此設定來確認下載訊息及傳送訊息的位置。</string> + <string name="public_chat_explainer">公開聊天室對所有 Nostr 使用者可見, 任何人皆可參與。非常適合圍繞特定主題的開放社群。 可透過在其 relay 上刪除貼文來進行內容管理</string> + <string name="public_chat_relays_explainer">請輸入 1–3 個託管此群組的 relay。 Nostr 客戶端使用此設定來確認下載訊息及傳送訊息的位置。</string> <string name="public_chat_relays_title">Relay</string> <string name="public_chat_title">公開聊天室元資料</string> <string name="public_chats">公開聊天室</string> @@ -2693,10 +2681,7 @@ <string name="read_only_user">只讀用戶</string> <string name="record_a_message_description">按住以錄製訊息</string> <string name="relay_chat">Relay 聊天</string> - <string name="relay_chat_explainer">Relay 聊天室是由其所屬 relay 管控的聊天群組。 - 所有 Nostr 使用者均可看見,任何人皆可參與。 - 非常適合圍繞特定主題的開放社群。部分群組為臨時性質, - 因此聊天訊息會隨時間消失</string> + <string name="relay_chat_explainer">Relay 聊天室是由其所屬 relay 管控的聊天群組。 所有 Nostr 使用者均可看見,任何人皆可參與。 非常適合圍繞特定主題的開放社群。部分群組為臨時性質, 因此聊天訊息會隨時間消失</string> <string name="relay_chat_title">Relay 聊天室</string> <string name="relay_management_tab_events">事件</string> <string name="relay_management_tab_ips">IP</string> diff --git a/commonsUI/src/commonMain/composeResources/values-zh/strings.xml b/commonsUI/src/commonMain/composeResources/values-zh/strings.xml index 643d37c01f..17be4c460b 100644 --- a/commonsUI/src/commonMain/composeResources/values-zh/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-zh/strings.xml @@ -337,7 +337,7 @@ <string name="new_community_rules_kinds_hint">轻触以允许一种类型。长按一种类型来设置每个事件的大小或每日帖子的限制。</string> <string name="new_community_rules_kind_custom_label">自定义类型</string> <string name="new_community_rules_kind_add">添加类型</string> - <string name="new_community_rules_limits_title"> %1$d 类型的限制</string> + <string name="new_community_rules_limits_title">%1$d 类型的限制</string> <string name="new_community_rules_limits_max_bytes">最大事件大小(字节)</string> <string name="new_community_rules_limits_max_per_day">每位作者每天最多发帖数</string> <string name="new_community_rules_limits_unlimited">留空为无限制</string> @@ -949,9 +949,7 @@ <string name="push_server_none_explainer">关闭推送通知</string> <string name="push_server_uses_app_explainer">使用应用 %1$s</string> <string name="push_server_install_app">推送通知设置</string> - <string name="push_server_install_app_description"> 若要接收推送通知,请安装任何支持 [Unified Push](https://unifiedpus)的应用, 例如 [Ntfy](https://ntfy.sh/)。 - 安装后,请在设置中选择你想要使用的应用程序。 - </string> + <string name="push_server_install_app_description">若要接收推送通知,请安装任何支持 [Unified Push](https://unifiedpus)的应用, 例如 [Ntfy](https://ntfy.sh/)。 安装后,请在设置中选择你想要使用的应用程序。</string> <string name="thread_title">主题帖</string> <string name="send_the_seller_a_message">向卖方发送消息</string> <string name="hi_there_is_this_still_available">你好,这个还能买到吗?</string> @@ -1801,15 +1799,8 @@ <string name="ots_explorer_api_label">Explorer API 基础URL</string> <string name="dvm_pay_invoice_from_dvm">支付来自DVM的发票</string> <string name="dvm_pay_amount_to_dvm">支付 %1$s sats 到 DVM</string> - <string xmlns:ns0="http://schemas.android.com/tools" name="account_backup_tips2_md" ns0:ignore="Typos">## 备份与安全提示 - \n\n你的帐户由一个私钥保护。私钥是以**nsec1**开头的随机字符串。任何拥有你的私钥的人都可以使用你的身份发布内容。 - \n\n- **不要**将你的私钥添加到任何你不信任的网站或软件,亦不要在网上公开。 - \n- Amethyst 开发人员**永远不会**要求你提供私钥。 - \n- **请**保留你的私钥的安全备份,以备帐户恢复。我们建议使用密码管理器。 - </string> - <string xmlns:ns0="http://schemas.android.com/tools" name="account_backup_tips3_md" ns0:ignore="Typos"> 为了额外的安全性,你可以用密码加密你的密钥。 此密钥以 **ncryptsec1** 开头,没有密码就不能使用。 - \n\n如果你丢失密码, 你将无法恢复你的密钥。 - </string> + <string xmlns:ns0="http://schemas.android.com/tools" name="account_backup_tips2_md" ns0:ignore="Typos">## 备份与安全提示 \n\n你的帐户由一个私钥保护。私钥是以**nsec1**开头的随机字符串。任何拥有你的私钥的人都可以使用你的身份发布内容。 \n\n- **不要**将你的私钥添加到任何你不信任的网站或软件,亦不要在网上公开。 \n- Amethyst 开发人员**永远不会**要求你提供私钥。 \n- **请**保留你的私钥的安全备份,以备帐户恢复。我们建议使用密码管理器。</string> + <string xmlns:ns0="http://schemas.android.com/tools" name="account_backup_tips3_md" ns0:ignore="Typos">为了额外的安全性,你可以用密码加密你的密钥。 此密钥以 **ncryptsec1** 开头,没有密码就不能使用。 \n\n如果你丢失密码, 你将无法恢复你的密钥。</string> <string name="use_direct_url">使用直接链接</string> <string name="nsite_title">静态网站: %1$s</string> <string name="napplet_card_title">nApple: %1$s</string> @@ -2054,15 +2045,7 @@ <string name="could_not_fetch_invoice_from_details">无法从 %1$s 获取发票:%2$s</string> <string name="could_not_open_the_compressed_file">无法打开压缩文件</string> <string name="couldnt_find_nwc_wallets">未找到钱包</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst 找不到任何支持 Nostr Wallet Connect (NWC) 的钱包应用. -\n\n你可以做的事情: -\n\t1. 验证 NWC 支持:仔细确认偏好的钱包应用和 Nostr Wallet Connect 兼容。 -\n\t2. 创建连接:前往钱包应用并创建新的 NWC 连接。 -\n\t3.链接到 Amethyst: 你的钱包通常会给你三个选项: -\n\t\t3.1 自动链接:自动打开并连接到 Amethyst 的按钮。如果是这样, 单击那个按钮,打开 Amethyst 并在新屏上按保存。 -\n\t\t3.2 手动链接: 你可以复制的连接 URI,单击这个屏幕上的粘贴图标直接输入它。 -\n\t\t3.3 二维码: 钱包会创建一个你可以从这个屏幕中的二维码按钮那里读取的二维码。 - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst 找不到任何支持 Nostr Wallet Connect (NWC) 的钱包应用. \n\n你可以做的事情: \n\t1. 验证 NWC 支持:仔细确认偏好的钱包应用和 Nostr Wallet Connect 兼容。 \n\t2. 创建连接:前往钱包应用并创建新的 NWC 连接。 \n\t3.链接到 Amethyst: 你的钱包通常会给你三个选项: \n\t\t3.1 自动链接:自动打开并连接到 Amethyst 的按钮。如果是这样, 单击那个按钮,打开 Amethyst 并在新屏上按保存。 \n\t\t3.2 手动链接: 你可以复制的连接 URI,单击这个屏幕上的粘贴图标直接输入它。 \n\t\t3.3 二维码: 钱包会创建一个你可以从这个屏幕中的二维码按钮那里读取的二维码。</string> <string name="create">创建</string> <string name="d">天</string> <string name="danger_zone">危险区</string> @@ -2745,11 +2728,8 @@ <string name="proxy_section">代理中继</string> <string name="proxy_section_explainer">聚合器中继是应用获取资讯源时使用的中继(例如:filter.nostr.wine),设置这个列表将覆盖信箱模型的行为,使得应用只会连接到这个列表中的中继以获取内容。</string> <string name="public_chat">公共聊天</string> - <string name="public_chat_explainer">公开聊天对所有 Nostr 用户都是可见的,任何人都可以: - 参与这些聊天。这对特定话题建立的开放式社区来说很重要 - 通过删除中继上的帖子实现内容审核控制</string> - <string name="public_chat_relays_explainer">使用 1~3 个中继托管这个群组。 - 让 Nostr 客户端知道应该使用这些中继配置来发送和下载消息。</string> + <string name="public_chat_explainer">公开聊天对所有 Nostr 用户都是可见的,任何人都可以: 参与这些聊天。这对特定话题建立的开放式社区来说很重要 通过删除中继上的帖子实现内容审核控制</string> + <string name="public_chat_relays_explainer">使用 1~3 个中继托管这个群组。 让 Nostr 客户端知道应该使用这些中继配置来发送和下载消息。</string> <string name="public_chat_relays_title">中继器</string> <string name="public_chat_title">公开聊天元数据</string> <string name="public_chats">公共聊天</string> @@ -2776,10 +2756,7 @@ <string name="read_only_user">仅读用户</string> <string name="record_a_message_description">点按以录制消息</string> <string name="relay_chat">中继聊天</string> - <string name="relay_chat_explainer">中继聊天是由聊天所属中继控制的聊天群组。 - Nostr 上的每个人都可以看到这些群组,任何人都可以加入群组。 - 这非常适合围绕特定主题而建立的开放社区。其中一些群组是临时的, - 因此聊天信息会随着时间的推移而消失。</string> + <string name="relay_chat_explainer">中继聊天是由聊天所属中继控制的聊天群组。 Nostr 上的每个人都可以看到这些群组,任何人都可以加入群组。 这非常适合围绕特定主题而建立的开放社区。其中一些群组是临时的, 因此聊天信息会随着时间的推移而消失。</string> <string name="relay_chat_title">中继聊天</string> <string name="relay_management_tab_events">事件</string> <string name="relay_management_tab_ips">IP地址</string> diff --git a/commonsUI/src/commonMain/composeResources/values/strings.xml b/commonsUI/src/commonMain/composeResources/values/strings.xml index f8a7c986c4..8422630d31 100644 --- a/commonsUI/src/commonMain/composeResources/values/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values/strings.xml @@ -660,17 +660,8 @@ <string name="mark_all_known_as_read">Mark all Known as read</string> <string name="mark_all_new_as_read">Mark all New as read</string> <string name="mark_all_as_read">Mark all as read</string> - <string name="account_backup_tips2_md"> - ## Key Backup and Safety Tips - \n\nYour account is secured by a secret key. The key is a long sequence of characters starting with **nsec1**. Anyone who has access to this secret key can post and change your identity. - \n\n- Do **not** put your secret key in any website or software you do not trust. - \n- Amethyst developers will **never** ask for your secret key. - \n- **Do** keep a secure backup of your secret key for account recovery. We recommend using a password manager. - </string> - <string name="account_backup_tips3_md"> - For additional security, you can encrypt your key with a password. This key starts with **ncryptsec1** and cannot be used without your password. - \n\nIf you lose your password, you will not be able to recover your key. - </string> + <string name="account_backup_tips2_md">## Key Backup and Safety Tips \n\nYour account is secured by a secret key. The key is a long sequence of characters starting with **nsec1**. Anyone who has access to this secret key can post and change your identity. \n\n- Do **not** put your secret key in any website or software you do not trust. \n- Amethyst developers will **never** ask for your secret key. \n- **Do** keep a secure backup of your secret key for account recovery. We recommend using a password manager.</string> + <string name="account_backup_tips3_md">For additional security, you can encrypt your key with a password. This key starts with **ncryptsec1** and cannot be used without your password. \n\nIf you lose your password, you will not be able to recover your key.</string> <string name="backup_keys_nudge_title">Back up your keys</string> <string name="backup_keys_nudge_body">Your secret key is the only way to access this account. If you lose it, it can never be recovered. Save it somewhere safe now.</string> <string name="backup_keys_nudge_backup_now">Back up now</string> @@ -1853,10 +1844,7 @@ <string name="push_server_none_explainer">Disables Push Notifications</string> <string name="push_server_uses_app_explainer">Uses app %1$s</string> <string name="push_server_install_app">Push Notification Setup</string> - <string name="push_server_install_app_description"> - To receive push notifications, install any app that supports [Unified Push](https://unifiedpush.org/), such as [Ntfy](https://ntfy.sh/). - After installing, select the app you want to use in the Settings. - </string> + <string name="push_server_install_app_description">To receive push notifications, install any app that supports [Unified Push](https://unifiedpush.org/), such as [Ntfy](https://ntfy.sh/). After installing, select the app you want to use in the Settings.</string> <string name="thread_title">Thread</string> <string name="send_the_seller_a_message">Send the seller a message</string> <string name="hi_there_is_this_still_available">Hi there, is this still available?</string> @@ -3629,15 +3617,7 @@ <string name="could_not_fetch_invoice_from_details">Could not fetch invoice from %1$s: %2$s</string> <string name="could_not_open_the_compressed_file">Could not open the compressed file</string> <string name="couldnt_find_nwc_wallets">Wallet not found</string> - <string name="couldnt_find_nwc_wallets_description">Amethyst couldn't find any wallet apps that support Nostr Wallet Connect (NWC). -\n\nWhat you can do: -\n\t1. Verify NWC Support: Double-check that your preferred wallet app is compatible with Nostr Wallet Connect. -\n\t2. Create Connection: Go to your wallet app and create a new NWC connection. -\n\t3. Link to Amethyst: Your wallet will usually give you three options: -\n\t\t3.1 Automatic Link: A button to automatically open and connect to Amethyst. If so, click that button, open Amethyst and hit save on the new screen. -\n\t\t3.2 Manual Link: A Connection URI you can copy and click the paste icon on this screen to directly input it. -\n\t\t3.3 QR Code: The wallet creates a QR code which you can read from the QR code button in this screen. - </string> + <string name="couldnt_find_nwc_wallets_description">Amethyst couldn't find any wallet apps that support Nostr Wallet Connect (NWC). \n\nWhat you can do: \n\t1. Verify NWC Support: Double-check that your preferred wallet app is compatible with Nostr Wallet Connect. \n\t2. Create Connection: Go to your wallet app and create a new NWC connection. \n\t3. Link to Amethyst: Your wallet will usually give you three options: \n\t\t3.1 Automatic Link: A button to automatically open and connect to Amethyst. If so, click that button, open Amethyst and hit save on the new screen. \n\t\t3.2 Manual Link: A Connection URI you can copy and click the paste icon on this screen to directly input it. \n\t\t3.3 QR Code: The wallet creates a QR code which you can read from the QR code button in this screen.</string> <string name="create">Create</string> <string name="d">d</string> <string name="danger_zone">Danger Zone</string> @@ -4773,11 +4753,8 @@ <string name="proxy_section">Proxy Relays</string> <string name="proxy_section_explainer">Aggregator relays the app must use to download your feeds from, like filter.nostr.wine. This replaces the outbox model and will make the app connect to only the relays in your lists.</string> <string name="public_chat">Public Chat</string> - <string name="public_chat_explainer">Public chats are visible to everyone on Nostr and anyone - can participate on them. They are great for open communities around specific topics. - Moderation can be controlled by deleting posts on its relays</string> - <string name="public_chat_relays_explainer">Insert between 1–3 relays that host this group. - Nostr clients use this setting to know where to download messages from and send your messages to.</string> + <string name="public_chat_explainer">Public chats are visible to everyone on Nostr and anyone can participate on them. They are great for open communities around specific topics. Moderation can be controlled by deleting posts on its relays</string> + <string name="public_chat_relays_explainer">Insert between 1–3 relays that host this group. Nostr clients use this setting to know where to download messages from and send your messages to.</string> <string name="public_chat_relays_title">Relays</string> <string name="public_chat_title">Public Chat Metadata</string> <string name="public_chats">Public Chats</string> @@ -4844,10 +4821,7 @@ <string name="relay_auth_search_keywords">auth authentication login relay sign verify nip-42 identity</string> <string name="relay_auth_settings_title">Relay login</string> <string name="relay_chat">Relay Chat</string> - <string name="relay_chat_explainer">Relay chats are chat groups controlled by their home relay. - They are visible to everyone on Nostr and anyone can participate on them. - They are great for open communities around specific topics. Some of these groups are ephemeral - and thus chat messages disappear over time</string> + <string name="relay_chat_explainer">Relay chats are chat groups controlled by their home relay. They are visible to everyone on Nostr and anyone can participate on them. They are great for open communities around specific topics. Some of these groups are ephemeral and thus chat messages disappear over time</string> <string name="relay_chat_title">Relay Chats</string> <string name="relay_explain_account_data">Your own profile, settings and drafts, on your home relays.</string> <string name="relay_explain_community_chats">The relays each community publishes its planes to.</string> diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ClickableLinks.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ClickableLinks.kt index 8a48da7f7d..c50279a930 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ClickableLinks.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ClickableLinks.kt @@ -87,7 +87,8 @@ fun ClickableEmail( textDecoration = if (underline) TextDecoration.Underline else null, modifier = modifier.pointerHoverIcon(PointerIcon.Hand).clickable { - runCatching { uriHandler.openUri("mailto:$display") } + // '%' is legal in an address but starts an escape in a mailto: URI (RFC 6068). + runCatching { uriHandler.openUri("mailto:" + display.replace("%", "%25")) } }, ) } diff --git a/desktopApp/plans/2026-06-21-napplet-desktop-host.md b/desktopApp/plans/2026-06-21-napplet-desktop-host.md index b7b53f4ce8..03be3b0bc8 100644 --- a/desktopApp/plans/2026-06-21-napplet-desktop-host.md +++ b/desktopApp/plans/2026-06-21-napplet-desktop-host.md @@ -19,7 +19,7 @@ doc is the map: what already works for free, what desktop must build, and the de `NappletIdentity`, `NappletRequest`/`NappletResponse`, the permissions `Ledger`/`Store`/`GrantState`, and the gateway **interfaces** (`NappletRelayGateway`, `NappletStorage`, `NappletWalletGateway`, `NappletResourceGateway`, `NappletUploadGateway`, `NappletIdentityGateway`, `NappletConsentPrompt`). -- **`NappletProtocolJson`** (`commons/jvmAndroid`) — the wire codec. Desktop's host marshals through +- **`NappletProtocolJson`** (`commons/commonMain`) — the wire codec. Desktop's host marshals through the same object, so request/result/push shapes can never drift between platforms. **The web contract (`commons/commonMain/composeResources/files/napplet/` + `NappletWebContract`)** — diff --git a/tools/strings-migrate/fix_escapes.py b/tools/strings-migrate/fix_escapes.py index 34a48089f8..65c7a7c566 100755 --- a/tools/strings-migrate/fix_escapes.py +++ b/tools/strings-migrate/fix_escapes.py @@ -19,6 +19,17 @@ It does not handle these Android conventions, so this script applies them itself \\n, \\t, \\uXXXX and \\\\ are deliberately left alone -- Compose already resolves them, and converting them here would double-process. +Whitespace is the other half. aapt collapses every whitespace run in an unquoted +value to one space and trims the ends; Compose keeps the XML text verbatim. A value +wrapped over several indented lines therefore showed on Android as one line, and in +Compose as a leading line break plus eight spaces -- which CommonMark renders as a +code block (`account_backup_tips2_md`). This script applies aapt's rule to runs that +contain a line break or tab, which only XML layout produces (a deliberate newline is +written \\n). In a translation it also trims a leading/trailing space the source +string does not have: Crowdin exports a translator's stray edge space, aapt used to +drop it, Compose would draw it (" miejsca zniknęły" beside "tematy zniknęły"). +Deliberate edge spaces (" and ") exist in the source too, and are kept. + Usage: fix_escapes.py <file-or-dir> [...] (idempotent; safe to re-run) """ import re @@ -35,9 +46,28 @@ TOOLS_ATTR = re.compile(r'\s+tools:[\w.-]+="[^"]*"') TOOLS_NS = re.compile(r'\s+xmlns:tools="[^"]*"') # a backslash escape NOT itself preceded by a backslash ANDROID_ONLY = re.compile(r"(?<!\\)\\(['\"?@])") +# A whitespace run that holds a line break or a tab: XML layout, never content. +LAYOUT_WS = re.compile(r"[ \t]*[\r\n\t][ \t\r\n]*") +STRING_NAME = re.compile(r'<string\b[^>]*\bname="([^"]+)"') -def fix_text(text: str, unwrap_quotes: bool = True) -> str: +def normalize_whitespace(text: str, source: str = None) -> str: + """Apply the aapt whitespace handling that Compose lacks. Idempotent. + + [source] is the default-locale value of the same key when [text] is a translation. + """ + if "<![CDATA[" in text: + return text + text = LAYOUT_WS.sub(lambda m: "" if m.start() == 0 or m.end() == len(text) else " ", text) + if source is not None: + if not source[:1].isspace(): + text = text.lstrip(" ") + if not source[-1:].isspace(): + text = text.rstrip(" ") + return text + + +def fix_text(text: str, unwrap_quotes: bool = True, source: str = None) -> str: """Convert one element's text. NOTE: quote-unwrapping is NOT idempotent and must run exactly once per file, at @@ -47,8 +77,9 @@ def fix_text(text: str, unwrap_quotes: bool = True) -> str: already-migrated files must therefore pass unwrap_quotes=False. """ if unwrap_quotes and len(text) >= 2 and text.startswith('"') and text.endswith('"'): - text = text[1:-1] - return ANDROID_ONLY.sub(r"\1", text) + # Quoted: aapt kept this whitespace exactly, so Compose keeps it too. + return ANDROID_ONLY.sub(r"\1", text[1:-1]) + return normalize_whitespace(ANDROID_ONLY.sub(r"\1", text), source) def strip_android_only_attrs(src: str) -> tuple: @@ -58,13 +89,31 @@ def strip_android_only_attrs(src: str) -> tuple: return out, n + m +def source_values(path: str) -> dict: + """The default-locale values beside a `values-<locale>/strings.xml`, else {}.""" + parent = os.path.dirname(os.path.abspath(path)) + if not os.path.basename(parent).startswith("values-"): + return {} + default = os.path.join(os.path.dirname(parent), "values", os.path.basename(path)) + if not os.path.exists(default): + return {} + out = {} + for m in STRING_EL.finditer(open(default, encoding="utf-8").read()): + name = STRING_NAME.match(m.group(1)) + if name: + out[name.group(1)] = m.group(2) + return out + + def fix_file(path: str, unwrap_quotes: bool = True) -> int: src = open(path, encoding="utf-8").read() + sources = source_values(path) changed = 0 def repl(m): nonlocal changed - fixed = fix_text(m.group(2), unwrap_quotes) + name = STRING_NAME.match(m.group(1)) + fixed = fix_text(m.group(2), unwrap_quotes, sources.get(name.group(1)) if name else None) if fixed != m.group(2): changed += 1 return m.group(1) + fixed + m.group(3) From e76566fba2ba3a6cdc90fb2e85c0b341b12bae10 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 22:14:38 +0000 Subject: [PATCH 21/60] fix: address review findings on the event rename branch - Deprecated aliases for merged classes now keep their old companion API: TokenEvent.build(encryptedContent), NutzapRedemptionEvent.build(nutzap, content), SoftwareReleaseEvent.build(appId, version, channel, assets) and buildDTag resolve to deprecated forwarders instead of an incompatible builder. Covered by DeprecatedMergedEventApiTest. - CashuWalletOps.redeemNutzap now tags the nutzap sender (NIP-61 ["p", sender]), using the notifySender helper from the merge. - TokenReference.parseFromTag requires a 64-char id, like ETag, so redeemedReferences() and redeemedNutzaps() agree; drop the per-tag list allocation. - Kind 30063 search: check content is non-blank before the two NIP-82 tag scans. - SoftwareApp: findAll/findLatest release lookups no longer re-run the NIP-82 and d-tag checks that the addressables scan already applied; cheap d-tag check first. - Deprecate asSoftwareRelease(): EventFactory already builds ReleaseArtifactSetEvent, and re-wrapping another kind breaks its id/sig. - Replace inline fully-qualified names touched by the rename with imports. - nostr-expert event-hierarchy doc: describe the real NIP-51 class hierarchy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015VvJ8XpeYqBe8bYT3bJ7UD --- .../references/event-hierarchy.md | 22 ++++-- .../amethyst/ui/note/types/SoftwareApp.kt | 22 +++--- .../create/NewCalendarCollectionViewModel.kt | 2 +- .../appfunctions/AmethystAppFunctions.kt | 3 +- .../commons/cashu/ops/CashuWalletOps.kt | 6 +- .../commons/viewmodels/NestViewModel.kt | 3 +- .../vitorpamplona/amethyst/desktop/Main.kt | 3 +- .../DesktopRelaySubscriptionsCoordinator.kt | 3 +- .../desktop/ui/note/DesktopRichText.kt | 3 +- .../release/Nip82ReleaseDetector.kt | 5 ++ .../release/SoftwareReleaseEvent.kt | 32 +++++++++ .../ReleaseArtifactSetEvent.kt | 5 +- .../nip60Cashu/history/TokenReference.kt | 6 +- .../redemption/NutzapRedemptionEvent.kt | 16 +++++ .../quartz/nip61Nutzaps/token/TokenEvent.kt | 16 +++++ .../SoftwareApplicationEventTest.kt | 9 +-- .../utils/DeprecatedMergedEventApiTest.kt | 68 +++++++++++++++++++ 17 files changed, 187 insertions(+), 37 deletions(-) create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/DeprecatedMergedEventApiTest.kt diff --git a/.claude/skills/nostr-expert/references/event-hierarchy.md b/.claude/skills/nostr-expert/references/event-hierarchy.md index cf6f6dfd12..a2c288c9de 100644 --- a/.claude/skills/nostr-expert/references/event-hierarchy.md +++ b/.claude/skills/nostr-expert/references/event-hierarchy.md @@ -95,14 +95,22 @@ class LongFormContentEvent(...) : BaseAddressableEvent(...) // Addressable via kind:pubkey:d-tag ``` -### Lists (kinds 10000-30004) +### Lists (NIP-51) +Each list kind is its own class under `nip51Lists/`, not a subtype of a shared list event. +Lists with private (NIP-44 encrypted) items extend one of two bases: ```kotlin -sealed class FollowSetEvent : BaseAddressableEvent { - object MuteList : FollowSetEvent(10000) - object PinList : FollowSetEvent(10001) - object BookmarkList : FollowSetEvent(10003) - // ... 18 list types total -} +// Base for sets (kind 30000-39999); some replaceable lists use it too (MuteListEvent, InterestListEvent) +abstract class PrivateTagArrayEvent(...) : BaseAddressableEvent(...) +// Replaceable lists (kind 10000-19999) +abstract class PrivateReplaceableTagArrayEvent(...) : BaseReplaceableEvent(...) + +class MuteListEvent(...) : PrivateTagArrayEvent(...) // kind 10000 +class PinListEvent(...) : BaseReplaceableEvent(...) // kind 10001, public only +class BookmarkListEvent(...) : PrivateReplaceableTagArrayEvent(...) // kind 10003 +class InterestListEvent(...) : PrivateTagArrayEvent(...) // kind 10015 +class FollowSetEvent(...) : PrivateTagArrayEvent(...) // kind 30000 follow sets +class BookmarkSetEvent(...) : PrivateTagArrayEvent(...) // kind 30003 bookmark sets +class StarterPackEvent(...) : BaseAddressableEvent(...) // kind 39089 starter packs ``` ## Event Interfaces diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt index 8067e9db46..2caa900e85 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt @@ -242,7 +242,7 @@ fun produceLatestReleaseVersion(app: SoftwareApplicationEvent): State<String?> { return flow.collectAsStateWithLifecycle(initialValue = null) } -fun findLatestNip82Release(app: SoftwareApplicationEvent): ReleaseArtifactSetEvent? = latestNip82Release(nip82ReleaseNotesFor(app), app) +fun findLatestNip82Release(app: SoftwareApplicationEvent): ReleaseArtifactSetEvent? = nip82ReleasesFor(app).maxByOrNull { it.createdAt } /** Picks the newest NIP-82 release for [app] out of an already-narrowed [notes] collection. */ private fun latestNip82Release( @@ -256,12 +256,13 @@ private fun latestNip82Release( } /** kind-30063 addressables authored by [app] whose `d` tag is `<app-id>@<version>`. */ -private fun nip82ReleaseNotesFor(app: SoftwareApplicationEvent): Set<Note> { +private fun nip82ReleasesFor(app: SoftwareApplicationEvent): List<ReleaseArtifactSetEvent> { val prefix = "${app.dTag()}@" - return LocalCache.addressables.filterIntoSet(ReleaseArtifactSetEvent.KIND, app.pubKey) { _, addr -> - val ev = addr.event ?: return@filterIntoSet false - ev.isNip82SoftwareRelease() && ev.dTag().startsWith(prefix) - } + return LocalCache.addressables + .filterIntoSet(ReleaseArtifactSetEvent.KIND, app.pubKey) { _, addr -> + val ev = addr.event ?: return@filterIntoSet false + ev.dTag().startsWith(prefix) && ev.isNip82SoftwareRelease() + }.mapNotNull { it.event as? ReleaseArtifactSetEvent } } /** @@ -270,15 +271,10 @@ private fun nip82ReleaseNotesFor(app: SoftwareApplicationEvent): Set<Note> { */ private fun Note.asNip82ReleaseFor(prefix: String): ReleaseArtifactSetEvent? { val ev = event as? ReleaseArtifactSetEvent ?: return null - return ev.takeIf { it.isNip82SoftwareRelease() && it.dTag().startsWith(prefix) } + return ev.takeIf { it.dTag().startsWith(prefix) && it.isNip82SoftwareRelease() } } -fun findAllNip82Releases(app: SoftwareApplicationEvent): List<ReleaseArtifactSetEvent> { - val prefix = "${app.dTag()}@" - return nip82ReleaseNotesFor(app) - .mapNotNull { it.asNip82ReleaseFor(prefix) } - .sortedByDescending { it.createdAt } -} +fun findAllNip82Releases(app: SoftwareApplicationEvent): List<ReleaseArtifactSetEvent> = nip82ReleasesFor(app).sortedByDescending { it.createdAt } @Composable fun AppIcon( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt index f389c7a9c1..24dace858c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt @@ -62,7 +62,7 @@ class NewCalendarCollectionViewModel : ViewModel() { private var dTag: String? = null /** The full original event in edit mode; needed to publish a NIP-09 deletion. */ - private var loadedEvent: com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent? = null + private var loadedEvent: CalendarCollectionEvent? = null val selectedAddresses = mutableStateListOf<Address>() val availableAppointments = mutableStateOf<List<OwnedAppointmentSummary>>(emptyList()) diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/appfunctions/AmethystAppFunctions.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/appfunctions/AmethystAppFunctions.kt index f14ec732b7..c865137a76 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/appfunctions/AmethystAppFunctions.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/appfunctions/AmethystAppFunctions.kt @@ -62,6 +62,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -1787,7 +1788,7 @@ class AmethystAppFunctions { lnAddress: String, sats: Long, comment: String, - zapRequest: com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent, + zapRequest: ZapRequestEvent, ): String { // Compute the LNURL-pay endpoint so we can ask the privacy-aware // HttpClient builder for the right OkHttpClient for that host. diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/ops/CashuWalletOps.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/ops/CashuWalletOps.kt index e26b1471d1..8262ec6188 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/ops/CashuWalletOps.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/ops/CashuWalletOps.kt @@ -57,6 +57,7 @@ import com.vitorpamplona.quartz.nip60Cashu.wallet.CashuWalletEvent import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent import com.vitorpamplona.quartz.nip61Nutzaps.info.tags.NutzapMintTag import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent +import com.vitorpamplona.quartz.nip61Nutzaps.redemption.notifySender import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent import com.vitorpamplona.quartz.utils.Log @@ -914,7 +915,10 @@ class CashuWalletOps( TokenReference(nutzap.id, null, TokenReference.MARKER_REDEEMED), ), signer = signer, - ) + ) { + // NIP-61: tag the nutzap sender so their client learns it was redeemed. + notifySender(EventHintBundle(nutzap)) + } val historyEvent = signer.sign(historyTemplate) publish(historyEvent) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModel.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModel.kt index faf5d970f1..e642ed3aa5 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModel.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModel.kt @@ -45,6 +45,7 @@ import com.vitorpamplona.nestsclient.connectReconnectingNestsSpeaker import com.vitorpamplona.nestsclient.transport.WebTransportFactory import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.collections.immutable.ImmutableSet @@ -629,7 +630,7 @@ class NestViewModel( * same window so the visual cadence matches the React button. */ fun onZapEvent( - event: com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent, + event: ZapReceiptEvent, nowSec: Long, windowSec: Long = REACTION_WINDOW_SEC, ) { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index a25cac2394..ce53aa2bde 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -166,6 +166,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent @@ -1926,7 +1927,7 @@ fun MainContent( // Process into chatroomList based on event type when (event) { - is com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent -> { + is EncryptedDmEvent -> { iAccount.chatroomList.addMessage( event.chatroomKey(iAccount.pubKey), note, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt index d72846949e..140f96d844 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt @@ -39,6 +39,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchAll import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException @@ -310,7 +311,7 @@ class DesktopRelaySubscriptionsCoordinator( ), // Zap receipts (kind 9735) targeting these notes Filter( - kinds = listOf(com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent.KIND), + kinds = listOf(ZapReceiptEvent.KIND), tags = mapOf("e" to noteIds), ), // Reposts (kind 6) targeting these notes diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt index 257baa799b..eb45da9db6 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt @@ -84,6 +84,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import java.awt.Toolkit import java.awt.datatransfer.StringSelection import java.net.URI @@ -376,7 +377,7 @@ private fun RenderBechSegment( .NAddress .parse(segment.segmentText) } - if (naddr != null && naddr.kind == com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent.KIND) { + if (naddr != null && naddr.kind == StarterPackEvent.KIND) { val followPacks = com.vitorpamplona.amethyst.desktop.ui.deck.LocalFollowPacksState.current val relayManager = com.vitorpamplona.amethyst.desktop.ui.deck.LocalRelayManager.current if (localCache != null && relayManager != null && followPacks != null) { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/Nip82ReleaseDetector.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/Nip82ReleaseDetector.kt index 088ad5934a..6ed0e63cf5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/Nip82ReleaseDetector.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/Nip82ReleaseDetector.kt @@ -44,4 +44,9 @@ fun Event.isNip82SoftwareRelease() = * Reinterprets `this` as a [ReleaseArtifactSetEvent]. Use [isNip82SoftwareRelease] * to gate the call. */ +@Deprecated( + "EventFactory already builds kind 30063 as ReleaseArtifactSetEvent, which exposes the NIP-82 fields. " + + "Re-wrapping an event of another kind yields one whose kind no longer matches its id and signature.", + ReplaceWith("this as ReleaseArtifactSetEvent", "com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent"), +) fun Event.asSoftwareRelease() = this as? ReleaseArtifactSetEvent ?: ReleaseArtifactSetEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/SoftwareReleaseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/SoftwareReleaseEvent.kt index 86712d1400..872b86630e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/SoftwareReleaseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/SoftwareReleaseEvent.kt @@ -20,7 +20,11 @@ */ package com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release +import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAssetEvent +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent +import com.vitorpamplona.quartz.utils.TimeUtils /** * NIP-82 software releases share kind 30063 with NIP-51 release artifact sets, and @@ -32,3 +36,31 @@ import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSet ReplaceWith("ReleaseArtifactSetEvent", "com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent"), ) typealias SoftwareReleaseEvent = ReleaseArtifactSetEvent + +/** The old `SoftwareReleaseEvent.buildDTag`. */ +@Deprecated( + "Use ReleaseArtifactSetEvent.buildSoftwareReleaseDTag.", + ReplaceWith("ReleaseArtifactSetEvent.buildSoftwareReleaseDTag(appId, version)", "com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent"), +) +fun ReleaseArtifactSetEvent.Companion.buildDTag( + appId: String, + version: String, +) = buildSoftwareReleaseDTag(appId, version) + +/** The old `SoftwareReleaseEvent.build`: a NIP-82 release, not the NIP-51 set builder. */ +@Deprecated( + "Use ReleaseArtifactSetEvent.buildSoftwareRelease.", + ReplaceWith( + "ReleaseArtifactSetEvent.buildSoftwareRelease(appId, version, channel, assets, releaseNotes, createdAt, initializer)", + "com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent", + ), +) +fun ReleaseArtifactSetEvent.Companion.build( + appId: String, + version: String, + channel: String, + assets: List<EventHintBundle<SoftwareAssetEvent>>, + releaseNotes: String = "", + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder<ReleaseArtifactSetEvent>.() -> Unit = {}, +) = buildSoftwareRelease(appId, version, channel, assets, releaseNotes, createdAt, initializer) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt index 0e8a9db9ab..8926ed25cb 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt @@ -82,8 +82,9 @@ class ReleaseArtifactSetEvent( } // Only NIP-82 releases carry release notes in `content`. A NIP-51 set may keep - // encrypted private items there, which must never reach the index. - private fun searchableReleaseNotes() = if (isNip82SoftwareRelease()) content.ifBlank { null } else null + // encrypted private items there, which must never reach the index. The blank + // check runs first so empty sets skip the two tag scans on every search. + private fun searchableReleaseNotes() = if (content.isNotBlank() && isNip82SoftwareRelease()) content else null override fun eventHints() = tags.mapNotNull(EventBookmark::parseAsHint) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip60Cashu/history/TokenReference.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip60Cashu/history/TokenReference.kt index fe16e631f2..ca73026eca 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip60Cashu/history/TokenReference.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip60Cashu/history/TokenReference.kt @@ -43,9 +43,11 @@ data class TokenReference( const val MARKER_REDEEMED = "redeemed" fun parseFromTag(tag: Array<String>): TokenReference? { - if (tag.size < 4 || tag[0] != "e") return null + // Same id rule as ETag.parse, so redeemedReferences() and the NIP-61 + // redeemedNutzaps() agree on which tags are valid. + if (tag.size < 4 || tag[0] != "e" || tag[1].length != 64) return null val marker = tag[3] - if (marker !in listOf(MARKER_CREATED, MARKER_DESTROYED, MARKER_REDEEMED)) return null + if (marker != MARKER_CREATED && marker != MARKER_DESTROYED && marker != MARKER_REDEEMED) return null return TokenReference( eventId = tag[1], relay = tag.getOrNull(2)?.ifEmpty { null }, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/redemption/NutzapRedemptionEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/redemption/NutzapRedemptionEvent.kt index 1382f57b12..0b2897a1cf 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/redemption/NutzapRedemptionEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/redemption/NutzapRedemptionEvent.kt @@ -44,6 +44,22 @@ fun CashuSpendingHistoryEvent.Companion.buildNutzapRedemption( initializer() } +/** The old `NutzapRedemptionEvent.build`. */ +@Deprecated( + "Use CashuSpendingHistoryEvent.buildNutzapRedemption.", + ReplaceWith( + "CashuSpendingHistoryEvent.buildNutzapRedemption(nutzap, encryptedContent, createdAt, initializer)", + "com.vitorpamplona.quartz.nip60Cashu.history.CashuSpendingHistoryEvent", + "com.vitorpamplona.quartz.nip61Nutzaps.redemption.buildNutzapRedemption", + ), +) +fun CashuSpendingHistoryEvent.Companion.build( + nutzap: EventHintBundle<NutzapEvent>, + encryptedContent: String, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder<CashuSpendingHistoryEvent>.() -> Unit = {}, +) = buildNutzapRedemption(nutzap, encryptedContent, createdAt, initializer) + /** * NIP-61 redemptions are NIP-60 spending history events (kind 7376). This used to be a * second class for the same kind, which `EventFactory` could never instantiate because diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/token/TokenEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/token/TokenEvent.kt index 0e72608c50..e48f186e62 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/token/TokenEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/token/TokenEvent.kt @@ -20,7 +20,10 @@ */ package com.vitorpamplona.quartz.nip61Nutzaps.token +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenEvent +import com.vitorpamplona.quartz.utils.TimeUtils /** * NIP-61 does not define its own kind 7375: redeemed nutzap proofs are stored as @@ -32,3 +35,16 @@ import com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenEvent ReplaceWith("CashuTokenEvent", "com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenEvent"), ) typealias TokenEvent = CashuTokenEvent + +/** The old `TokenEvent.build` took content that was already encrypted. */ +@Deprecated( + "Use CashuTokenEvent.build(tokenContent, signer), which encrypts the token content.", + ReplaceWith("CashuTokenEvent.build(tokenContent, signer)", "com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenEvent"), +) +fun CashuTokenEvent.Companion.build( + encryptedContent: String, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder<CashuTokenEvent>.() -> Unit = {}, +) = eventTemplate(KIND, encryptedContent, createdAt) { + initializer() +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/SoftwareApplicationEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/SoftwareApplicationEventTest.kt index 6f5166d193..39bd9b45f4 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/SoftwareApplicationEventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/SoftwareApplicationEventTest.kt @@ -32,7 +32,6 @@ import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAss import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.apkCertificateHash import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.platform import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.versionCode -import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.asSoftwareRelease import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.isNip82SoftwareRelease import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.shared.Platform import com.vitorpamplona.quartz.nip01Core.core.Event @@ -137,13 +136,12 @@ class SoftwareApplicationEventTest { } @Test - fun asSoftwareRelease_exposesParsedFields() { - val event = - Event( + fun releaseArtifactSet_exposesParsedNip82Fields() { + val release = + ReleaseArtifactSetEvent( id = "0".repeat(64), pubKey = "1".repeat(64), createdAt = 0, - kind = ReleaseArtifactSetEvent.KIND, tags = arrayOf( arrayOf("d", "com.example.app@1.0.0"), @@ -154,7 +152,6 @@ class SoftwareApplicationEventTest { content = "Initial release", sig = "", ) - val release = event.asSoftwareRelease() assertEquals("com.example.app", release.appId()) assertEquals("1.0.0", release.version()) assertEquals("main", release.channel()) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/DeprecatedMergedEventApiTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/DeprecatedMergedEventApiTest.kt new file mode 100644 index 0000000000..740e463ab3 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/DeprecatedMergedEventApiTest.kt @@ -0,0 +1,68 @@ +/* + * 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. + */ +@file:Suppress("DEPRECATION") + +package com.vitorpamplona.quartz.utils + +import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.SoftwareReleaseEvent +import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.build +import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.buildDTag +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip60Cashu.history.CashuSpendingHistoryEvent +import com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenEvent +import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent +import com.vitorpamplona.quartz.nip61Nutzaps.redemption.NutzapRedemptionEvent +import com.vitorpamplona.quartz.nip61Nutzaps.redemption.build +import com.vitorpamplona.quartz.nip61Nutzaps.token.TokenEvent +import com.vitorpamplona.quartz.nip61Nutzaps.token.build +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * The classes merged into another class keep their old companion calls compiling + * through the deprecated typealiases. If any of these stop resolving, external + * Quartz users break on upgrade. + */ +class DeprecatedMergedEventApiTest { + private val nutzap = NutzapEvent("a".repeat(64), "1".repeat(64), 1, emptyArray(), "", "b".repeat(128)) + + @Test + fun tokenEventBuildStillTakesEncryptedContent() { + val template = TokenEvent.build("encrypted") + assertEquals(CashuTokenEvent.KIND, template.kind) + assertEquals("encrypted", template.content) + } + + @Test + fun nutzapRedemptionBuildStillAddsRedeemedAndSenderTags() { + val template = NutzapRedemptionEvent.build(EventHintBundle(nutzap), "encrypted") + assertEquals(CashuSpendingHistoryEvent.KIND, template.kind) + assertEquals(listOf("e", "p"), template.tags.map { it[0] }) + } + + @Test + fun softwareReleaseBuildStillBuildsNip82Release() { + assertEquals("app@1.0", SoftwareReleaseEvent.buildDTag("app", "1.0")) + val template = SoftwareReleaseEvent.build("app", "1.0", "main", emptyList()) + assertEquals("app@1.0", template.tags.first { it[0] == "d" }[1]) + assertEquals("1.0", template.tags.first { it[0] == "version" }[1]) + } +} From 78986bd38c79f2b1aca2e882d255d7ada94650cb Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 22:23:27 +0000 Subject: [PATCH 22/60] fix(quartz): never cut an astral character in half in the tagging d MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An audit pass over this branch found the `d16` truncation unpublishable for a target whose `d` has an astral character straddling the 16th UTF-16 code unit. Reproduced end to end before fixing: - the id is hashed from the in-memory string, lone high surrogate and all, giving 7f0bec88...; - UTF-8 has no encoding for half a character, so the bytes that reach the socket carry 0x3F ("?") in its place — not U+FFFD; - a relay parsing those bytes re-hashes a4bc430d... and rejects the event as id-mismatched. The first attempt to reproduce this compared string to string and came back clean; it only shows up once the comparison is at the byte level, which is where the relay reads. `d16` now drops a dangling high surrogate, giving 15 units instead of 16. The draft calls `d16` decoration that carries no uniqueness, and dropping half a character stays deterministic, so replaceability is unaffected and `hash8` still separates the targets. Pinned two ways: the `d` holds no unpaired surrogate, and it survives a UTF-8 round trip byte for byte. Also softens the KDoc on `prefix`, which claimed two targets can never collide. `hash8` is 32 bits by the draft's own widths, so what this actually removes is the old rule's *certain* collision between any two of an author's addressable events; a birthday-bound residue remains and is upstream's to widen, not ours. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXvKXakvup4inNFfAhhr4L --- .../taggings/EventTagging.kt | 28 ++++++++++++++++--- .../decentralizedLists/AuditRegressionTest.kt | 24 ++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/EventTagging.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/EventTagging.kt index 8a45a5ef82..69d42d9956 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/EventTagging.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/taggings/EventTagging.kt @@ -43,9 +43,13 @@ import com.vitorpamplona.quartz.utils.sha256.sha256 sealed interface TaggingTarget { /** * What stands for this target in the assertion `d`: `<id8>` for a plain event, - * `<author8>-<d16>-<hash8>` for an addressable one. Two distinct targets must never produce - * the same one — kind 39999 is addressable, so taggings sharing a `d` do not sit side by - * side: the later one replaces the earlier. + * `<author8>-<d16>-<hash8>` for an addressable one. Kind 39999 is addressable, so two + * taggings that share a `d` do not sit side by side — the later one replaces the earlier. + * + * The draft fixes these widths, so the separation is 32 bits of hash rather than a guarantee: + * it removes the old rule's *certain* collision between any two of an author's events, and + * leaves a birthday-bound residue that the draft accepts. Widening it is an upstream change, + * not ours. */ val prefix: String @@ -62,7 +66,23 @@ sealed interface TaggingTarget { * An empty `dTag` yields an empty `d16`, i.e. `<author8>--<hash8>`, which the draft spells * out. A plain event uses its own id (below): that already covers the whole event. */ - override val prefix get() = "${address.pubKeyHex.take(AUTHOR_CHARS)}-${address.dTag.take(D_TAG_CHARS)}-${hash8()}" + override val prefix get() = "${address.pubKeyHex.take(AUTHOR_CHARS)}-${d16()}-${hash8()}" + + /** + * The draft counts `d16` in UTF-16 code units, which can cut an astral character in half + * and leave a dangling high surrogate. That is not survivable: the id is hashed from the + * intact char, but UTF-8 encoding the event for the wire has no encoding for half a + * character and substitutes `?`, so the relay re-hashes different bytes, gets a different + * id, and rejects the event. Verified end to end — see the test. + * + * So the half character is dropped, giving 15 units instead of 16. `d16` is decoration + * the draft says carries no uniqueness, and dropping it stays deterministic, so nothing + * that matters is lost; `hash8` still separates the targets. + */ + private fun d16(): String { + val cut = address.dTag.take(D_TAG_CHARS) + return if (cut.lastOrNull()?.isHighSurrogate() == true) cut.dropLast(1) else cut + } // Lowercase hex of the UTF-8 coordinate exactly as the `a` tag carries it: no trimming, // no normalization, no reordering. Hex.encode already emits lower case. diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/AuditRegressionTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/AuditRegressionTest.kt index fbf7384959..08b44cca86 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/AuditRegressionTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/AuditRegressionTest.kt @@ -183,6 +183,30 @@ class AuditRegressionTest { ) } + // Truncating at 16 UTF-16 code units can cut an astral character in half. The id is hashed + // from the intact char while the wire bytes get `?` in its place, so the relay re-hashes + // different bytes and rejects the event. The half character is dropped instead. + @Test + fun aDTagCutMidAstralCharacterDoesNotLeaveHalfACharacterInTheD() { + // 15 ASCII then an emoji: the 16th code unit is the high surrogate of the pair. + val dTag = "a".repeat(15) + "\uD83D\uDE00" + "tail" + val d = EventTagging.dTag("awesome-tag", TaggingTarget.ByAddress(Address(39999, bob, dTag)), alice) + + assertFalse(d.any { it.isHighSurrogate() || it.isLowSurrogate() }, "the `d` still carries half a character: $d") + // 15 units of decoration, not 16, and hash8 is over the untruncated coordinate. + assertEquals("event-tag-awesome-tag-bbbbbbbb-${"a".repeat(15)}-15c09d87-aaaaaaaa", d) + } + + // The property the above protects: what is hashed for the id must survive UTF-8 encoding, + // or the relay recomputes a different id. Pins the bytes, not just the string. + @Test + fun theDSurvivesAUtf8RoundTripByteForByte() { + val dTag = "a".repeat(15) + "\uD83D\uDE00" + "tail" + val d = EventTagging.dTag("awesome-tag", TaggingTarget.ByAddress(Address(39999, bob, dTag)), alice) + + assertEquals(d, d.encodeToByteArray().decodeToString(), "the `d` does not survive UTF-8 encoding") + } + // A plain event is still named by its own id: it already covers the whole event, and hashing // it would only cost a round of SHA-256 per assertion built. The `e` branch is unchanged. @Test From dc5932a8121801f023389db8cf5c83b850cd158d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona <vitor@vitorpamplona.com> Date: Sat, 26 Sep 2026 18:26:22 -0400 Subject: [PATCH 23/60] feat(browser): name the browser the hand-off tile will open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Other browser" says what it is not. When the system will name a default, the tile now says "Open in Chrome" — which is also the answer to "where does this go?", the question the tile exists to answer. Knowing the name took a `<queries>` entry. Android 11+ package visibility answers `resolveActivity` with nothing for a scheme the manifest does not declare, so the app could see every payment scheme it had asked about and not https. One entry alongside those, read-only: it names the target, it does not change where anything goes. `defaultBrowserName` is null whenever a name would be a promise we cannot keep — no default set (the system resolves to its own picker, which would otherwise be labelled "Android System"), the only handler is Amethyst, or visibility still hides it. The tile falls back to "Open in browser" and the hand-off keeps going through a chooser either way, so a wrong answer here costs a label and never a mis-launch. Only the full-screen browser passes a name so far; the embedded tab still shows the generic label. Verified on an SM-T220: the tile reads "Open in Chrome", on one line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- amethyst/src/main/AndroidManifest.xml | 11 +++ .../composeResources/values/strings.xml | 1 + .../commons/browser/ui/pill/BrowserPill.kt | 9 ++- .../browser/ui/pill/BrowserPillModel.kt | 7 ++ .../amethyst/napplethost/DefaultBrowser.kt | 69 +++++++++++++++++++ .../napplethost/NappletBrowserActivity.kt | 1 + 6 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/DefaultBrowser.kt diff --git a/amethyst/src/main/AndroidManifest.xml b/amethyst/src/main/AndroidManifest.xml index dd42a938dd..d2b9dabc65 100644 --- a/amethyst/src/main/AndroidManifest.xml +++ b/amethyst/src/main/AndroidManifest.xml @@ -17,6 +17,17 @@ <action android:name="android.intent.action.TTS_SERVICE" /> </intent> + <!-- So the browser's hand-off tile can name the browser it would open. + Android 11+ package visibility answers resolveActivity with nothing + for a scheme that is not declared here, so without this the tile can + only ever say "Open in browser". Read-only: it names the target, the + hand-off itself still goes through a chooser. --> + <intent> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="https" /> + </intent> + <!-- NIP-A3 payment targets. Android 11+ package visibility means queryIntentActivities returns NOTHING for a scheme not declared here, so without these the zap picker's pay-to chip is invisible on every diff --git a/commonsUI/src/commonMain/composeResources/values/strings.xml b/commonsUI/src/commonMain/composeResources/values/strings.xml index d1e8fac7c4..206367ecd7 100644 --- a/commonsUI/src/commonMain/composeResources/values/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values/strings.xml @@ -5374,6 +5374,7 @@ <string name="browser_pill_add_home">Add to Home</string> <string name="browser_pill_desktop">Desktop site</string> <string name="browser_pill_other_browser">Open in browser</string> + <string name="browser_pill_other_browser_named">Open in %1$s</string> <string name="browser_pill_full_screen">Full screen</string> <string name="browser_pill_left_site">You left %1$s</string> <string name="browser_pill_back_to_app">Back to app</string> diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPill.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPill.kt index 2c93718755..ca09cc1bc8 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPill.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPill.kt @@ -82,6 +82,7 @@ import com.vitorpamplona.amethyst.commons.resources.browser_pill_close import com.vitorpamplona.amethyst.commons.resources.browser_pill_decision_allowed import com.vitorpamplona.amethyst.commons.resources.browser_pill_decision_blocked import com.vitorpamplona.amethyst.commons.resources.browser_pill_left_site +import com.vitorpamplona.amethyst.commons.resources.browser_pill_other_browser_named import com.vitorpamplona.amethyst.commons.resources.browser_pill_permission_state import com.vitorpamplona.amethyst.commons.resources.browser_pill_privacy import com.vitorpamplona.amethyst.commons.resources.browser_pill_site_settings_none @@ -365,10 +366,14 @@ private fun TileGrid( actions.chunked(columns).forEach { rowActions -> Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { rowActions.forEach { action -> + // The hand-off tile names the browser it will actually open when the + // system will name one; "Open in browser" is what is left when the + // answer is a chooser. See BrowserPillUi.defaultBrowserName. + val named = ui.defaultBrowserName?.takeIf { action == Action.OPEN_IN_BROWSER_APP } ActionTile( symbol = pillSymbolFor(action) ?: MaterialSymbols.Info, - label = stringRes(pillTileLabelFor(action)), - description = stringRes(pillLabelFor(action)), + label = named?.let { stringRes(Res.string.browser_pill_other_browser_named, it) } ?: stringRes(pillTileLabelFor(action)), + description = named?.let { stringRes(Res.string.browser_pill_other_browser_named, it) } ?: stringRes(pillLabelFor(action)), onClick = { onAction(action) }, selected = when (action) { diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillModel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillModel.kt index 1d3ea5b87a..e4ef87c8ee 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillModel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillModel.kt @@ -44,6 +44,13 @@ data class BrowserPillUi( val consoleErrors: Int = 0, /** Answers this site already has (camera / mic / location), for the site-settings summary. */ val sitePermissions: Map<BrowserSitePermission, BrowserSitePermission.Decision> = emptyMap(), + /** + * The default browser's name, when the system will name one and it is not us. + * + * Null means the hand-off shows a chooser, and the tile has to stay "Open in browser": + * no default is set, the device hides it, or the only handler is Amethyst itself. + */ + val defaultBrowserName: String? = null, ) { val security: BrowserChrome.Security get() = BrowserChrome.security(chrome) val host: String get() = BrowserChrome.displayHost(chrome.url) diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/DefaultBrowser.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/DefaultBrowser.kt new file mode 100644 index 0000000000..a15b5b7d40 --- /dev/null +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/DefaultBrowser.kt @@ -0,0 +1,69 @@ +/* + * 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.napplethost + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import androidx.core.net.toUri + +/** + * Who "open in a browser" would actually hand the page to. + * + * Only used to say so on the tile. The hand-off itself still goes through a chooser, so a wrong + * or missing answer here costs a label, never a mis-launch. + */ +object DefaultBrowser { + /** A probe URL: the scheme is what selects browsers, the host is never contacted. */ + private val PROBE = Intent(Intent.ACTION_VIEW, "https://example.com".toUri()).addCategory(Intent.CATEGORY_BROWSABLE) + + /** + * The default browser's app label, or null when the tile should stay generic. + * + * Null in three cases that all mean the same thing to a reader — you are going to get a + * chooser, so do not promise a name: + * - no default is set, and the system resolves to its own picker; + * - the only handler is Amethyst, so "open in a browser" means anything but us; + * - package visibility hides it. Android 11+ answers `resolveActivity` with nothing unless + * the manifest declares a matching `<queries>` entry, which is why one exists for + * http/https alongside the payment schemes. + */ + fun label(context: Context): String? = + runCatching { + val pm = context.packageManager + + @Suppress("DEPRECATION") + val match: ResolveInfo = pm.resolveActivity(PROBE, PackageManager.MATCH_DEFAULT_ONLY) ?: return null + val pkg = match.activityInfo?.packageName ?: return null + // The system picker resolves for everything; naming it would be a lie. + if (pkg == context.packageName || isResolver(match)) return null + match.loadLabel(pm).toString().takeIf { it.isNotBlank() } + }.getOrNull() + + /** + * Whether this is Android's own chooser rather than a browser. + * + * `resolveActivity` returns the resolver when several apps match and none is default; it + * reports `exported=false` from the `android` package, which is the cheap way to tell. + */ + private fun isResolver(info: ResolveInfo): Boolean = info.activityInfo?.packageName == "android" +} diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt index 558c750bb7..a32f8f27d1 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt @@ -1337,6 +1337,7 @@ class NappletBrowserActivity : ComponentActivity() { torOn = if (proxyPort > 0) useTor else null, ), isFavorite = intent.getBooleanExtra(EXTRA_IS_FAVORITE, false), + defaultBrowserName = DefaultBrowser.label(this), ), listener = chromeListener, ) From 5175d2ad01d0bde70e4f68f61c2074fd3118de68 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 22:50:27 +0000 Subject: [PATCH 24/60] refactor(commonsUI): move ScreenLayout, TimeAgo, the theme core and the single-blocker components Continues the move of shared UI out of the Android app. - ScreenLayout: the tier rule, CappedScreenContent and the pane widths go to commonsUI. The window-size-class breakpoints (600/840dp) are inlined, since material3-window-size-class is app-only; ScreenLayoutTest's 17 cases pass against them. Android still reads LocalConfiguration and asks the shared rememberScreenLayoutSpec(width, height). - Viewer chrome inset + controls visibility, SlidingCarousel, AudioWaveformReadOnly (local enums replace the Android-only library's), FileAttachmentCard (extractFilename -> commons util) move to commonsUI. PdfFetcher moves to commonsUI jvmAndroid with an explicit diskCache instead of Amethyst.instance; commonsUI jvmAndroid gains okhttp-coroutines, which every OkHttp-shipping module already has. - TimeAgo: the formatter, ToggleableTimeAgoText/TimeAgo, NowProvider and the labels move to commonsUI over four expects (skeleton formatter, calendar day, time-of-day formatter, short relative span). Android keeps its exact behaviour (getBestDateTimePattern, the system 12/24h setting, DateUtils); the JVM uses DateTimeFormatter.ofLocalizedPattern, iOS NSDateFormatter. DateSkeletonFormatterTest pins the JVM side. - Theme: the colour scheme, typography, density and providers become the shared AmethystMaterialTheme; the app's AmethystTheme reads the prefs, calls it, and still tints the system bars. The Vico chart colours stay app-side. The sweep plan records the round and a fresh measurement of what is left. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjQQN9CgWHVtNCSWqnKoqY --- .../playback/composable/wavefront/Waveform.kt | 2 +- .../vitorpamplona/amethyst/ui/MainActivity.kt | 2 +- .../ui/actions/uploads/VoiceMessagePreview.kt | 2 +- .../ui/components/FullScreenViewerChrome.kt | 98 +----------- .../ui/components/ZoomableContentDialog.kt | 2 + .../ui/components/pdf/PdfPreviewCard.kt | 13 +- .../ui/components/pdf/PdfViewerDialog.kt | 6 +- .../ui/layouts/ChatHeaderLayoutPreview.kt | 2 +- .../amethyst/ui/layouts/ScreenLayout.kt | 118 +------------- .../layouts/listItem/SlimListItemPreview.kt | 2 +- .../ui/navigation/NavigationEffects.kt | 2 +- .../ui/navigation/drawer/DrawerContent.kt | 2 +- .../amethyst/ui/note/BadgeCompose.kt | 1 + .../amethyst/ui/note/NoteCompose.kt | 5 +- .../amethyst/ui/note/RelayCompose.kt | 1 + .../expiration/ExpirationDatePicker.kt | 2 +- .../note/creators/polls/PollDeadlinePicker.kt | 2 +- .../creators/scheduling/ScheduleAtPicker.kt | 2 +- .../zappolls/ZapPollDeadlinePicker.kt | 2 +- .../amethyst/ui/note/elements/DisplayOts.kt | 2 +- .../amethyst/ui/note/types/FileHeader.kt | 2 +- .../amethyst/ui/note/types/Fundraiser.kt | 2 +- .../amethyst/ui/note/types/Library.kt | 2 +- .../amethyst/ui/note/types/LongForm.kt | 2 +- .../loggedIn/backups/BackupConflictCards.kt | 2 +- .../backups/BackupConflictReviewScreen.kt | 2 +- .../loggedIn/buzz/AgentWorkBoardScreen.kt | 4 +- .../screen/loggedIn/buzz/BuzzDmListScreen.kt | 6 +- .../ui/screen/loggedIn/buzz/BuzzImportRow.kt | 2 +- .../ui/screen/loggedIn/buzz/JobBoardScreen.kt | 4 +- .../loggedIn/buzz/WorkflowRunBoardScreen.kt | 4 +- .../cordnGroup/CordnCreateGroupScreen.kt | 2 +- .../chats/cordnGroup/CordnMessageRow.kt | 4 +- .../loggedIn/chats/feed/ChatDeliveryTicks.kt | 6 +- .../screen/loggedIn/chats/feed/ChatTimeAgo.kt | 6 +- .../chats/feed/NewDateOrSubjectDivisor.kt | 2 +- .../chats/feed/layouts/ChatGroupPosition.kt | 2 +- .../marmotGroup/MarmotGroupInfoScreen.kt | 2 +- .../marmotGroup/MarmotGroupListScreen.kt | 4 +- .../send/PrivateMessageEditFieldRow.kt | 2 +- .../concord/ConcordChannelListScreen.kt | 2 +- .../concord/ConcordHomeScreen.kt | 2 +- .../header/LongPublicChatChannelHeader.kt | 2 +- .../header/LongLiveActivityChannelHeader.kt | 2 +- .../relayGroup/RelayGroupChannelListScreen.kt | 6 +- .../relayGroup/RelayGroupDiscoveryScreen.kt | 2 +- .../chats/rooms/ChatroomHeaderCompose.kt | 4 +- .../screen/loggedIn/gitRepo/GitItemListRow.kt | 2 +- .../loggedIn/gitRepo/GitRepositoryHomeUi.kt | 2 +- .../screen/loggedIn/nests/NestsFeedLoaded.kt | 2 +- .../notifications/NotificationSidePanel.kt | 2 +- .../podcasts/PodcastEpisodeListItem.kt | 4 +- .../podcasts/PodcastTrailerListItem.kt | 4 +- .../polls/results/PollResultsScreen.kt | 8 +- .../profile/header/DrawAdditionalInfo.kt | 2 +- .../relayauth/RelayAuthSettingsScreen.kt | 2 +- .../loggedIn/relays/RelayInformationScreen.kt | 2 +- .../relays/eventsync/EventSyncScreen.kt | 2 +- .../scheduledposts/ScheduledPostsScreen.kt | 4 +- .../loggedIn/settings/AppSettingsScreen.kt | 4 +- .../settings/cordn/CordnCoordinatorsScreen.kt | 2 +- .../settings/nip46/Nip46ActivityUi.kt | 2 +- .../loggedIn/threadview/ThreadFeedView.kt | 4 +- .../screen/loggedIn/video/UserCardHeader.kt | 2 +- .../vitorpamplona/amethyst/ui/theme/Theme.kt | 135 +++------------- .../amethyst/ui/layouts/ScreenLayoutTest.kt | 2 + .../2026-08-30-commons-migration-sweep.md | 69 ++++++++ .../amethyst/commons/util/MimeTypeLabels.kt | 7 + commonsUI/build.gradle.kts | 2 + .../ui/note/PlatformDateFormat.android.kt | 78 +++++++++ .../ui/components/AudioWaveformReadOnly.kt | 10 +- .../ui/components/FileAttachmentCard.kt | 6 +- .../commons}/ui/components/SlidingCarousel.kt | 2 +- .../commons/ui/components/ViewerChrome.kt | 130 +++++++++++++++ .../commons/ui/layouts/ScreenLayout.kt | 121 ++++++++++++++ .../commons/ui/note/PlatformDateFormat.kt | 56 +++++++ .../commons}/ui/note/TimeAgoFormatter.kt | 126 +++++---------- .../commons}/ui/note/elements/NowProvider.kt | 2 +- .../commons}/ui/note/elements/TimeAgo.kt | 29 ++-- .../commons/ui/theme/AmethystTheme.kt | 149 ++++++++++++++++++ .../commons/ui/note/PlatformDateFormat.ios.kt | 76 +++++++++ .../commons/service}/pdf/PdfFetcher.kt | 9 +- .../commons/ui/note/CalendarDay.jvmAndroid.kt | 28 ++++ .../commons/ui/note/PlatformDateFormat.jvm.kt | 74 +++++++++ .../ui/note/DateSkeletonFormatterTest.kt | 97 ++++++++++++ 85 files changed, 1077 insertions(+), 531 deletions(-) create mode 100644 commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/components/AudioWaveformReadOnly.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/components/FileAttachmentCard.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/components/SlidingCarousel.kt (98%) create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ViewerChrome.kt create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/TimeAgoFormatter.kt (75%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/elements/NowProvider.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/note/elements/TimeAgo.kt (88%) create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/AmethystTheme.kt create mode 100644 commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components => commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service}/pdf/PdfFetcher.kt (92%) create mode 100644 commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/CalendarDay.jvmAndroid.kt create mode 100644 commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.jvm.kt create mode 100644 commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateSkeletonFormatterTest.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt index be8bdd5d03..e4bc115142 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt @@ -36,9 +36,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.media3.common.Player import com.linc.audiowaveform.infiniteLinearGradient +import com.vitorpamplona.amethyst.commons.ui.components.AudioWaveformReadOnly import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState import com.vitorpamplona.amethyst.service.playback.composable.WaveformData -import com.vitorpamplona.amethyst.ui.components.AudioWaveformReadOnly @Composable fun Waveform( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 194a0f8180..e21a488fe6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.navigation.routeFor import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.commons.ui.note.elements.NowProvider import com.vitorpamplona.amethyst.debugState import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService @@ -43,7 +44,6 @@ import com.vitorpamplona.amethyst.ui.navigation.findParameterValue import com.vitorpamplona.amethyst.ui.navigation.findQueryParameterValue import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.navigation.routes.routeForPointer -import com.vitorpamplona.amethyst.ui.note.elements.NowProvider import com.vitorpamplona.amethyst.ui.screen.AccountScreen import com.vitorpamplona.amethyst.ui.theme.AmethystTheme import com.vitorpamplona.quartz.buzz.invite.BuzzInviteLink diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt index 768bed0ade..6db110e268 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt @@ -57,8 +57,8 @@ import com.vitorpamplona.amethyst.commons.resources.re_record import com.vitorpamplona.amethyst.commons.resources.record_a_message import com.vitorpamplona.amethyst.commons.resources.recording_indicator_description import com.vitorpamplona.amethyst.commons.resources.remove +import com.vitorpamplona.amethyst.commons.ui.components.AudioWaveformReadOnly import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.ui.components.AudioWaveformReadOnly import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.delay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/FullScreenViewerChrome.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/FullScreenViewerChrome.kt index 8814b623dd..96c0050c96 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/FullScreenViewerChrome.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/FullScreenViewerChrome.kt @@ -24,9 +24,6 @@ import android.Manifest import android.os.Build import android.view.Window import android.widget.Toast -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.snap -import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.ExperimentalLayoutApi @@ -47,19 +44,13 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat @@ -75,6 +66,7 @@ import com.vitorpamplona.amethyst.commons.resources.download_to_phone import com.vitorpamplona.amethyst.commons.resources.media_download_has_started_toast import com.vitorpamplona.amethyst.commons.resources.quick_action_share import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent +import com.vitorpamplona.amethyst.commons.ui.components.animatedViewerChromeInset import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp @@ -83,29 +75,12 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeoutOrNull // Chrome shared by the full-screen media viewers -- the zoomable image/video dialog and the PDF // viewer. Both are opened the same way (tap a media card in a feed), so they immerse, auto-hide, // and lay their controls out identically. -// Opening churn -- insets arriving, then the bars being hidden -- must not look like a user -// gesture, so the row snaps through it and only animates afterwards. -private const val CONTROLS_SETTLE_BEFORE_ANIMATING_MS = 350L - -// Roughly the system bars' own show/hide duration, so the row travels with them rather than -// trailing after they have already arrived. -private const val CONTROLS_SLIDE_MS = 200 - -// Keeps the row off the screen edge -- and off the rounded corners -- while the bars are hidden. -private val VIEWER_CHROME_EDGE_GAP = 16.dp - -// How long the controls stay up before the viewer fades them out on its own. -private const val CONTROLS_AUTO_HIDE_DELAY_MS = 2000L - /** * Goes fully immersive for as long as the viewer is on screen: hides both OS bars and restores them * on the way out. BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE lets the user swipe to peek the bars back. @@ -123,77 +98,6 @@ fun ImmersiveSystemBarsEffect(window: Window?) { } } -/** - * Visibility of the viewer controls: they start on screen, fade out on their own after - * [CONTROLS_AUTO_HIDE_DELAY_MS], and the caller flips the returned state on tap. - * - * [holdOpen] freezes the timer while something anchored to the controls -- the share sheet, say -- - * is up, and re-arms it once that closes. [armed] withholds the countdown until there is something - * to look at, so a viewer that spends three seconds fetching its media doesn't reveal the first - * frame with the controls already gone. - * - * A tap that brings the controls back deliberately gets no timer: the user asked for them, so they - * stay until tapped away. That is why the countdown races the controls going away rather than just - * sleeping -- a timer left over from an earlier show would otherwise wipe controls the user tapped - * back up in the meantime. - */ -@Composable -fun rememberViewerControlsVisibility( - holdOpen: Boolean, - armed: Boolean = true, -): MutableState<Boolean> { - val visible = remember { mutableStateOf(true) } - - LaunchedEffect(armed, holdOpen) { - if (!armed || holdOpen) return@LaunchedEffect - - val hiddenFirst = - withTimeoutOrNull(CONTROLS_AUTO_HIDE_DELAY_MS) { - snapshotFlow { visible.value }.first { !it } - } - - if (hiddenFirst == null) visible.value = false - } - - return visible -} - -/** - * How far the viewer chrome sits from a screen edge: the system bar's own height while the bar is - * on screen, and a thin constant once it is hidden -- so the chrome follows the bar instead of - * reserving space for one that is not there. - * - * This only works because the viewer asks for BEHAVIOR_DEFAULT rather than transient bars. Under - * BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE a peeked bar is painted over the content and dispatches no - * insets at all -- `systemBars` stays 0 and `isVisible` stays false the whole time it is on screen - * -- so nothing here could react to it. - * - * The value is animated, but snapped for [CONTROLS_SETTLE_BEFORE_ANIMATING_MS] after the chrome - * appears. Opening moves the inset twice for reasons the user did not cause: the window has not - * been told its insets yet (they read 0), and ImmersiveSystemBarsEffect hides the bars from a - * DisposableEffect that runs after composition. Animating either would play a slide on open. - */ -@Composable -fun animatedViewerChromeInset(atBottom: Boolean): Dp { - val density = LocalDensity.current - val bars = WindowInsets.systemBars - val barPx = if (atBottom) bars.getBottom(density) else bars.getTop(density) - val target = with(density) { maxOf(barPx, VIEWER_CHROME_EDGE_GAP.roundToPx()).toDp() } - - var settled by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - delay(CONTROLS_SETTLE_BEFORE_ANIMATING_MS) - settled = true - } - - val animated by animateDpAsState( - targetValue = target, - animationSpec = if (settled) tween(durationMillis = CONTROLS_SLIDE_MS) else snap(), - label = "viewerChromeInset", - ) - return animated -} - /** * Lays a viewer control row along a screen edge -- the top by default, the bottom when [atBottom]. * diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt index c108571f73..3d9d867150 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt @@ -80,8 +80,10 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlPdf import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo +import com.vitorpamplona.amethyst.commons.ui.components.SlidingCarousel import com.vitorpamplona.amethyst.commons.ui.components.getActivityWindow import com.vitorpamplona.amethyst.commons.ui.components.getDialogWindow +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewerControlsVisibility import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier import com.vitorpamplona.amethyst.model.MediaAspectRatioCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt index 1228c7a635..4b72f34f30 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt @@ -44,16 +44,19 @@ import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalWindowInfo import androidx.core.graphics.createBitmap +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.richtext.MediaUrlPdf +import com.vitorpamplona.amethyst.commons.service.pdf.PdfFetcher +import com.vitorpamplona.amethyst.commons.ui.components.FileAttachmentRow import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.Size40dp import com.vitorpamplona.amethyst.commons.ui.theme.Size6dp import com.vitorpamplona.amethyst.commons.ui.theme.innerPostModifier +import com.vitorpamplona.amethyst.commons.util.extractFilename import com.vitorpamplona.amethyst.model.MediaAspectRatioCache import com.vitorpamplona.amethyst.ui.components.ClickableUrl -import com.vitorpamplona.amethyst.ui.components.FileAttachmentRow import com.vitorpamplona.amethyst.ui.components.ShareMediaAction import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.utils.Log @@ -155,7 +158,7 @@ private fun LoadedPdfPreviewCard( value = try { PdfFetcher - .fetchSnapshot(content.url) { url -> + .fetchSnapshot(content.url, Amethyst.instance.diskCache) { url -> accountViewModel.httpClientBuilder.okHttpClientForPreview(url) }.use { snapshot -> withContext(Dispatchers.IO) { @@ -327,10 +330,4 @@ internal fun cappedRenderSize( return w to h } -internal fun extractFilename(url: String): String { - val afterQuery = url.substringBefore('?').substringBefore('#') - val name = afterQuery.substringAfterLast('/', afterQuery) - return if (name.isBlank()) url else name -} - internal fun pageCountLabel(pageCount: Int): String = if (pageCount == 1) "1 page" else "$pageCount pages" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt index e7c14e71ce..e063369bc7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt @@ -61,8 +61,11 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.core.graphics.createBitmap import coil3.disk.DiskCache +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.richtext.MediaUrlPdf +import com.vitorpamplona.amethyst.commons.service.pdf.PdfFetcher import com.vitorpamplona.amethyst.commons.ui.components.getDialogWindow +import com.vitorpamplona.amethyst.commons.ui.components.rememberViewerControlsVisibility import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.components.ImmersiveSystemBarsEffect @@ -70,7 +73,6 @@ import com.vitorpamplona.amethyst.ui.components.ViewerBackButton import com.vitorpamplona.amethyst.ui.components.ViewerControlsRow import com.vitorpamplona.amethyst.ui.components.ViewerSaveToGalleryButton import com.vitorpamplona.amethyst.ui.components.ViewerShareButton -import com.vitorpamplona.amethyst.ui.components.rememberViewerControlsVisibility import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException @@ -194,7 +196,7 @@ private fun PdfViewerContent( try { withContext(Dispatchers.IO) { val snapshot = - PdfFetcher.fetchSnapshot(content.url) { url -> + PdfFetcher.fetchSnapshot(content.url, Amethyst.instance.diskCache) { url -> accountViewModel.httpClientBuilder.okHttpClientForPreview(url) } try { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayoutPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayoutPreview.kt index 1f49f5ac52..c2b95a98e3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayoutPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayoutPreview.kt @@ -38,12 +38,12 @@ import com.vitorpamplona.amethyst.commons.resources.profile_banner import com.vitorpamplona.amethyst.commons.ui.components.NewItemsBubble import com.vitorpamplona.amethyst.commons.ui.layouts.ChatHeaderLayout import com.vitorpamplona.amethyst.commons.ui.layouts.listItem.SlimListItem +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpModifier import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayout.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayout.kt index bd5e34e558..4c36ffe9bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayout.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayout.kt @@ -20,126 +20,14 @@ */ package com.vitorpamplona.amethyst.ui.layouts -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.widthIn -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.WindowSizeClass -import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.ui.layouts.NavigationStyle import com.vitorpamplona.amethyst.commons.ui.layouts.ScreenLayoutSpec +import com.vitorpamplona.amethyst.commons.ui.layouts.rememberScreenLayoutSpec -/** - * Minimum window width for the docked notification panel: a leading navigation pane, a - * readable center pane and the panel ([NotificationPanelWidth]) only coexist comfortably from - * a landscape-tablet-sized window up. Sized against the widest leading pane, the permanent - * drawer ([PermanentDrawerWidth]); the rail is narrower, so a railed window that clears this - * gets a roomier center pane rather than a tighter one. - */ -private const val NOTIFICATION_PANEL_MIN_WINDOW_DP = 1200 - -val PermanentDrawerWidth = 300.dp - -val NotificationPanelWidth = 360.dp - -/** - * Maximum width of a screen's content column inside a wide center pane. Every NavHost - * destination is wrapped in [CappedScreenContent] (via the builders in NavigationEffects), - * so the whole screen — top bar, tabs, feed, settings rows — shares one centered reading - * column instead of stretching across the pane. Screens that genuinely need the full pane - * (Messages' two-pane split, the embedded browser surfaces) opt out at registration. - */ -val FeedContentMaxWidth = 600.dp - -/** - * Minimum window height for the docked drawer. Higher than Material's 480dp Compact/Medium - * height boundary on purpose: the permanent drawer's own header — banner, avatar, status - * editor, follower counts — fills most of a ~540dp column before the first navigation row, so - * below this the rail shows more of the menu than the dock does. - */ -private const val DOCK_MIN_WINDOW_HEIGHT_DP = 600 - -/** - * The navigation tier for a window of this shape. - * - * The dock is not a width decision. A tablet is past the Expanded breakpoint in both - * orientations, so keying on width alone pins 300dp of menu open in portrait with no closed - * state to fall back on (issue #4024). It docks only when the window is wide, landscape, and - * tall enough for the drawer's own content to be usable; everything else that is not Compact - * falls through to the rail, which pairs with the existing swipe-in modal drawer. - * - * A square window counts as landscape and docks; `Configuration.ORIENTATION_LANDSCAPE` - * breaks that tie the other way, so the two disagree at exactly width == height. - */ -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) -internal fun decideNavigationStyle( - windowWidthDp: Int, - windowHeightDp: Int, -): NavigationStyle { - val widthSizeClass = - WindowSizeClass - .calculateFromSize(DpSize(windowWidthDp.dp, windowHeightDp.dp)) - .widthSizeClass - - return when { - widthSizeClass == WindowWidthSizeClass.Expanded && - windowWidthDp >= windowHeightDp && - windowHeightDp >= DOCK_MIN_WINDOW_HEIGHT_DP -> NavigationStyle.PERMANENT_DRAWER - widthSizeClass != WindowWidthSizeClass.Compact -> NavigationStyle.NAV_RAIL - else -> NavigationStyle.BOTTOM_BAR - } -} - -/** - * Whether the window is wide enough to dock the notification feed beside the content. - * - * Deliberately not keyed on [NavigationStyle]: a wide portrait window now gets the rail, and - * gating on the dock would strip a panel it has today. - */ -internal fun hasRoomForNotificationPanel(windowWidthDp: Int): Boolean = windowWidthDp >= NOTIFICATION_PANEL_MIN_WINDOW_DP - -/** - * Centers a destination's content at [FeedContentMaxWidth]. The outer box paints the theme - * background so the gutters match the screens' own surfaces; on Compact windows the cap is - * wider than the pane and this is a visual no-op. - */ -@Composable -fun CappedScreenContent(content: @Composable () -> Unit) { - Box( - modifier = - Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background), - contentAlignment = Alignment.TopCenter, - ) { - Box( - Modifier - .widthIn(max = FeedContentMaxWidth) - .fillMaxSize(), - ) { - content() - } - } -} - +/** Android's window size is the configuration's, which already excludes system decorations. */ @Composable fun rememberScreenLayoutSpec(): ScreenLayoutSpec { val configuration = LocalConfiguration.current - val windowWidthDp = configuration.screenWidthDp - val windowHeightDp = configuration.screenHeightDp - return remember(windowWidthDp, windowHeightDp) { - ScreenLayoutSpec( - navigationStyle = decideNavigationStyle(windowWidthDp, windowHeightDp), - hasRoomForNotificationPanel = hasRoomForNotificationPanel(windowWidthDp), - ) - } + return rememberScreenLayoutSpec(configuration.screenWidthDp, configuration.screenHeightDp) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/listItem/SlimListItemPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/listItem/SlimListItemPreview.kt index f4529fc097..2243aa9c6c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/listItem/SlimListItemPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/listItem/SlimListItemPreview.kt @@ -38,12 +38,12 @@ import com.vitorpamplona.amethyst.commons.resources.profile_banner import com.vitorpamplona.amethyst.commons.ui.components.NewItemsBubble import com.vitorpamplona.amethyst.commons.ui.layouts.ChatHeaderLayout import com.vitorpamplona.amethyst.commons.ui.layouts.listItem.SlimListItem +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpModifier import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NavigationEffects.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NavigationEffects.kt index 4f630d4b99..d7e44af2db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NavigationEffects.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NavigationEffects.kt @@ -40,7 +40,7 @@ import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.vitorpamplona.amethyst.ui.layouts.CappedScreenContent +import com.vitorpamplona.amethyst.commons.ui.layouts.CappedScreenContent import java.util.concurrent.ConcurrentHashMap import kotlin.reflect.KClass diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt index 2fe3758aa3..55c1b8a449 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt @@ -119,6 +119,7 @@ import com.vitorpamplona.amethyst.commons.resources.show_npub_as_a_qr_code import com.vitorpamplona.amethyst.commons.resources.status_update import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPostStatus import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage +import com.vitorpamplona.amethyst.commons.ui.layouts.PermanentDrawerWidth import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji @@ -150,7 +151,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsFollowerCount import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses -import com.vitorpamplona.amethyst.ui.layouts.PermanentDrawerWidth import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarCatalog import com.vitorpamplona.amethyst.ui.navigation.bottombars.NavBarItemDef import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt index a0373864e2..ff1515cf19 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt @@ -44,6 +44,7 @@ import com.vitorpamplona.amethyst.commons.model.navigation.routeFor import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.new_badge_award_notif import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.timeAgo import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 08ae330cde..bf7f3246d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -96,6 +96,9 @@ import com.vitorpamplona.amethyst.commons.ui.note.RenderCashuMint import com.vitorpamplona.amethyst.commons.ui.note.RenderFedimint import com.vitorpamplona.amethyst.commons.ui.note.RenderMintRecommendation import com.vitorpamplona.amethyst.commons.ui.note.elements.StaleRelayHint +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle +import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.state.produceCachedStateAsync import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -133,8 +136,6 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayReward import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton import com.vitorpamplona.amethyst.ui.note.elements.Reward import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayBroadcastRelayList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt index bf580ec3d1..c964eb47d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt @@ -42,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.add import com.vitorpamplona.amethyst.commons.resources.posts_received import com.vitorpamplona.amethyst.commons.resources.remove +import com.vitorpamplona.amethyst.commons.ui.note.timeAgo import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.commons.ui.theme.ButtonPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt index 0a6874d75f..d1775e6f0f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt @@ -63,10 +63,10 @@ import com.vitorpamplona.amethyst.commons.resources.expiration_date_select import com.vitorpamplona.amethyst.commons.resources.expiration_expires_in import com.vitorpamplona.amethyst.commons.resources.expiration_time import com.vitorpamplona.amethyst.commons.resources.next +import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot import com.vitorpamplona.quartz.utils.TimeUtils import java.time.Instant import java.time.ZoneId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt index e3ced94604..3c98337d8e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/polls/PollDeadlinePicker.kt @@ -58,8 +58,8 @@ import com.vitorpamplona.amethyst.commons.resources.confirm import com.vitorpamplona.amethyst.commons.resources.next import com.vitorpamplona.amethyst.commons.resources.poll_closing_date_time import com.vitorpamplona.amethyst.commons.resources.poll_closing_in +import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.quartz.utils.TimeUtils import java.time.Instant diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/scheduling/ScheduleAtPicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/scheduling/ScheduleAtPicker.kt index 779ad35dbb..b7b183eedd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/scheduling/ScheduleAtPicker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/scheduling/ScheduleAtPicker.kt @@ -76,10 +76,10 @@ import com.vitorpamplona.amethyst.commons.resources.schedule_post_time_label import com.vitorpamplona.amethyst.commons.resources.schedule_post_warning_multi import com.vitorpamplona.amethyst.commons.resources.schedule_post_warning_single import com.vitorpamplona.amethyst.commons.resources.schedule_post_warning_title +import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot import com.vitorpamplona.quartz.utils.TimeUtils import java.time.DayOfWeek import java.time.Instant diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt index 6ae96c6d6b..d53f4c10e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/ZapPollDeadlinePicker.kt @@ -58,8 +58,8 @@ import com.vitorpamplona.amethyst.commons.resources.confirm import com.vitorpamplona.amethyst.commons.resources.next import com.vitorpamplona.amethyst.commons.resources.poll_closing_date_time import com.vitorpamplona.amethyst.commons.resources.poll_closing_in +import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.quartz.utils.TimeUtils import java.time.Instant diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt index 623ea87a6c..40de13910d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt @@ -29,9 +29,9 @@ import com.vitorpamplona.amethyst.commons.resources.ots_info_pending_description import com.vitorpamplona.amethyst.commons.resources.ots_info_title import com.vitorpamplona.amethyst.commons.resources.timestamp_pending_short import com.vitorpamplona.amethyst.commons.ui.note.HeaderPill +import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.ui.note.LoadOts -import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import java.text.SimpleDateFormat import java.util.Date diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt index 81a4a54709..c1a7d5b569 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileHeader.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlPdf import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser -import com.vitorpamplona.amethyst.ui.components.FileAttachmentCard +import com.vitorpamplona.amethyst.commons.ui.components.FileAttachmentCard import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Fundraiser.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Fundraiser.kt index 564ee7077d..3a34469f92 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Fundraiser.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Fundraiser.kt @@ -51,6 +51,7 @@ import com.vitorpamplona.amethyst.commons.resources.preview_card_image_for import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.CopyIcon +import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size18Modifier import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText @@ -59,7 +60,6 @@ import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags -import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.experimental.agora.FundraiserEvent import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Library.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Library.kt index ca09e1fbc1..18d383e4b4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Library.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Library.kt @@ -68,6 +68,7 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlPdf import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.commons.ui.components.FileAttachmentCard import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -76,7 +77,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.util.countToHumanReadableBytes import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent -import com.vitorpamplona.amethyst.ui.components.FileAttachmentCard import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.ZoomableContentView diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt index 7e87980caa..463d760f48 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt @@ -52,6 +52,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.long_form_reading_minutes import com.vitorpamplona.amethyst.commons.resources.preview_card_image_for import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Font10SP import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp @@ -63,7 +64,6 @@ import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay import com.vitorpamplona.amethyst.ui.note.WatchAuthor import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt index 769c911ae4..465bfe8e8d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictCards.kt @@ -95,8 +95,8 @@ import com.vitorpamplona.amethyst.commons.resources.backup_conflict_count_remove import com.vitorpamplona.amethyst.commons.resources.backup_conflict_title import com.vitorpamplona.amethyst.commons.resources.backup_review_changed_by_other_app import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListDiff import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListDiff diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt index 6497b54846..e7cd48f968 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictReviewScreen.kt @@ -90,6 +90,7 @@ import com.vitorpamplona.amethyst.commons.resources.backup_review_title_updated import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.util.toShortDisplay @@ -101,7 +102,6 @@ import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay -import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt index 54ae79e5a1..580444c5c0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt @@ -90,11 +90,11 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt index 4c8804f15c..341b508ca2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt @@ -82,15 +82,15 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_dm_more import com.vitorpamplona.amethyst.commons.resources.buzz_dm_new import com.vitorpamplona.amethyst.commons.resources.buzz_dm_title import com.vitorpamplona.amethyst.commons.resources.cancel -import com.vitorpamplona.amethyst.commons.resources.now import com.vitorpamplona.amethyst.commons.resources.remove_from_messages import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeAgoLabels +import com.vitorpamplona.amethyst.commons.ui.note.timeAgoShort import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName import com.vitorpamplona.amethyst.ui.note.UserPicture -import com.vitorpamplona.amethyst.ui.note.timeAgoShort import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.isValid @@ -256,7 +256,7 @@ private fun DmRowCard( } Spacer(Modifier.width(8.dp)) Text( - text = timeAgoShort(row.lastActivity, stringRes(Res.string.now)), + text = timeAgoShort(row.lastActivity, rememberTimeAgoLabels()), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt index 5fe26fb721..53fd592704 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzImportRow.kt @@ -54,11 +54,11 @@ import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChann import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.relay_group_member_count import com.vitorpamplona.amethyst.commons.resources.relay_group_no_messages_yet +import com.vitorpamplona.amethyst.commons.ui.note.timeAgo import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName -import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.buzzTimelinePreviewSummary import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupCardWarmupSubscription diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/JobBoardScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/JobBoardScreen.kt index 2f3505c4ab..aed7f61cfb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/JobBoardScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/JobBoardScreen.kt @@ -88,12 +88,12 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.buzz_job_board_title import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/WorkflowRunBoardScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/WorkflowRunBoardScreen.kt index 42717bf0f3..48e3bdf00b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/WorkflowRunBoardScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/WorkflowRunBoardScreen.kt @@ -160,12 +160,12 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_workflow_waiting_for_ap import com.vitorpamplona.amethyst.commons.resources.buzz_workflow_waiting_on import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnCreateGroupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnCreateGroupScreen.kt index 0b78b0ea5e..6479e51eb5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnCreateGroupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnCreateGroupScreen.kt @@ -118,10 +118,10 @@ import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable_deta import com.vitorpamplona.amethyst.commons.resources.cordn_member_count import com.vitorpamplona.amethyst.commons.ui.components.EmptyState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.model.cordn.CordnCoverage import com.vitorpamplona.amethyst.model.cordn.CordnGroupCreation import com.vitorpamplona.amethyst.ui.note.UserPicture -import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.ui.pluralStringRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.observeUserNameByHex diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt index 031512faea..6d98298451 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt @@ -89,6 +89,8 @@ import com.vitorpamplona.amethyst.commons.resources.today import com.vitorpamplona.amethyst.commons.ui.components.ClickableBox import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle +import com.vitorpamplona.amethyst.commons.ui.note.elements.ToggleableTimeAgoText import com.vitorpamplona.amethyst.commons.ui.theme.Font12SP import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer @@ -98,8 +100,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.note.QuickActionAlertDialog import com.vitorpamplona.amethyst.ui.note.UserPicture -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle -import com.vitorpamplona.amethyst.ui.note.elements.ToggleableTimeAgoText import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ActionTile import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ChatChipFlowRow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt index d5d5e83096..3457109346 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatDeliveryTicks.kt @@ -47,7 +47,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -70,6 +69,7 @@ import com.vitorpamplona.amethyst.commons.resources.chat_delivery_sending import com.vitorpamplona.amethyst.commons.resources.close import com.vitorpamplona.amethyst.commons.ui.components.ClickableBox import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.timeAbsolute import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Font12SP import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp @@ -78,7 +78,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.allGoodColor import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay -import com.vitorpamplona.amethyst.ui.note.timeAbsolute import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -203,9 +202,8 @@ private fun ChatDeliveryDetailDialog( ) { // Absolute timestamp header (the chat time no longer toggles to absolute // on tap — it opens this dialog instead). - val context = LocalContext.current Text( - text = timeAbsolute(baseNote.createdAt(), context, prefix = "").trim(), + text = timeAbsolute(baseNote.createdAt(), prefix = "").trim(), color = MaterialTheme.colorScheme.placeholderText, fontSize = Font12SP, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt index 7f0627d365..8b3328cfdd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt @@ -40,15 +40,15 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.expiration_date_label import com.vitorpamplona.amethyst.commons.resources.relay_group_pinned_content_description import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle +import com.vitorpamplona.amethyst.commons.ui.note.elements.ToggleableTimeAgoText +import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Font12SP import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.note.elements.DisplayLocation import com.vitorpamplona.amethyst.ui.note.elements.DisplayPoW -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle -import com.vitorpamplona.amethyst.ui.note.elements.ToggleableTimeAgoText -import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt index b72eaa2e2a..355576e500 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt @@ -27,8 +27,8 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.never import com.vitorpamplona.amethyst.commons.resources.today +import com.vitorpamplona.amethyst.commons.ui.note.dateFormatter import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.ui.note.dateFormatter import com.vitorpamplona.quartz.nip14Subject.subject @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatGroupPosition.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatGroupPosition.kt index d942a54dd9..b710a72044 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatGroupPosition.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatGroupPosition.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.ui.note.dateFormatter import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeMe import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeMeBottom import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeMeMiddle @@ -34,7 +35,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeThem import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeThemBottom import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeThemMiddle import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeThemTop -import com.vitorpamplona.amethyst.ui.note.dateFormatter import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip14Subject.subject import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt index 66c97b3f0d..7321b36d9f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupInfoScreen.kt @@ -131,6 +131,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.timeAgo import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -144,7 +145,6 @@ import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState -import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt index 4c8d52184f..7894287c53 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt @@ -84,13 +84,13 @@ import com.vitorpamplona.amethyst.commons.resources.marmot_tab_new_requests_coun import com.vitorpamplona.amethyst.commons.resources.marmot_unread_messages import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle +import com.vitorpamplona.amethyst.commons.ui.note.elements.ToggleableTimeAgoText import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle -import com.vitorpamplona.amethyst.ui.note.elements.ToggleableTimeAgoText import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.hasEncryptedMediaV2 import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.hasMip04Media diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index 49d996db90..bb752cb228 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -64,6 +64,7 @@ import com.vitorpamplona.amethyst.commons.resources.this_message_will_disappear_ import com.vitorpamplona.amethyst.commons.resources.upload_without_encryption_warning import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldBorder @@ -86,7 +87,6 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.note.showCount -import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.RoomChatFileUploadDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt index 3075b9a87a..d95788cda4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt @@ -103,10 +103,10 @@ import com.vitorpamplona.amethyst.commons.resources.quick_action_share import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar +import com.vitorpamplona.amethyst.commons.ui.note.timeAgo import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar -import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelPreviewLoader import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt index d95ba0659d..95a86e69f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordHomeScreen.kt @@ -71,13 +71,13 @@ import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncIma import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar +import com.vitorpamplona.amethyst.commons.ui.note.timeAgo import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar -import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription import com.vitorpamplona.quartz.concord.cord02Community.ImagePointer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt index 3f2a7a7496..9a7cd2b7f1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt @@ -52,6 +52,7 @@ import com.vitorpamplona.amethyst.commons.resources.groups_no_descriptor import com.vitorpamplona.amethyst.commons.resources.owner import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.elements.NormalTimeAgo import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -66,7 +67,6 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton -import com.vitorpamplona.amethyst.ui.note.elements.NormalTimeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.LinkChatButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.MuteChatButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt index acfaebc195..fc50788692 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt @@ -50,6 +50,7 @@ import com.vitorpamplona.amethyst.commons.resources.created_at import com.vitorpamplona.amethyst.commons.resources.groups_no_descriptor import com.vitorpamplona.amethyst.commons.resources.owner import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.elements.NormalTimeAgo import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.Size25dp @@ -63,7 +64,6 @@ import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton -import com.vitorpamplona.amethyst.ui.note.elements.NormalTimeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt index c6e2de0394..0b62d8571f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt @@ -85,7 +85,6 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_dm_see_all_count import com.vitorpamplona.amethyst.commons.resources.buzz_dm_title import com.vitorpamplona.amethyst.commons.resources.buzz_forum_create_title import com.vitorpamplona.amethyst.commons.resources.buzz_import_loading -import com.vitorpamplona.amethyst.commons.resources.now import com.vitorpamplona.amethyst.commons.resources.relay_group_channels_empty import com.vitorpamplona.amethyst.commons.resources.relay_group_channels_not_nip29 import com.vitorpamplona.amethyst.commons.resources.relay_group_create_title @@ -100,6 +99,8 @@ import com.vitorpamplona.amethyst.commons.resources.relay_tor_clearnet_title import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeAgoLabels +import com.vitorpamplona.amethyst.commons.ui.note.timeAgoShort import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -109,7 +110,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.UserPicture -import com.vitorpamplona.amethyst.ui.note.timeAgoShort import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzAddPeopleDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz.BuzzDmListViewModel @@ -885,7 +885,7 @@ private fun BuzzDmInlineRow( } if (row.lastActivity > 0) { Text( - text = timeAgoShort(row.lastActivity, stringRes(Res.string.now)), + text = timeAgoShort(row.lastActivity, rememberTimeAgoLabels()), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupDiscoveryScreen.kt index afdf3562b4..3ebf4f1f44 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupDiscoveryScreen.kt @@ -77,6 +77,7 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.timeAgo import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -96,7 +97,6 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.FeedFilterSpinner import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon import com.vitorpamplona.amethyst.ui.note.UserPicture -import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.dal.relayGroupDiscoveryChannelFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.dal.toGroupConstraints diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index f758b7bdcc..5556468cd8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -111,6 +111,8 @@ import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncIma import com.vitorpamplona.amethyst.commons.ui.layouts.ChatHeaderLayout import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.HeaderPill +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle +import com.vitorpamplona.amethyst.commons.ui.note.elements.ToggleableTimeAgoText import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.AccountPictureModifier @@ -133,8 +135,6 @@ import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContentOrNull import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.note.ObserveDraftEvent -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle -import com.vitorpamplona.amethyst.ui.note.elements.ToggleableTimeAgoText import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.buzzTimelinePreviewSummary import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.observeUserNameByHex diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitItemListRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitItemListRow.kt index 059df45250..abf6b78359 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitItemListRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitItemListRow.kt @@ -58,6 +58,7 @@ import com.vitorpamplona.amethyst.commons.resources.git_untitled import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.StatusKind +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding @@ -71,7 +72,6 @@ import com.vitorpamplona.amethyst.ui.note.WatchAuthor import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.ui.note.clickableNoteModifier import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryHomeUi.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryHomeUi.kt index 382157f68d..6e86a1266f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryHomeUi.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepo/GitRepositoryHomeUi.kt @@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.commons.resources.git_repo_stat_tags import com.vitorpamplona.amethyst.commons.resources.git_repo_stat_updated import com.vitorpamplona.amethyst.commons.resources.git_untitled import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.grayText @@ -73,7 +74,6 @@ import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.ReactionsRow -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip34Git.git.GitCommit diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt index 52ca972185..cddb8b18e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt @@ -74,6 +74,7 @@ import com.vitorpamplona.amethyst.commons.resources.wallet_loading import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled import com.vitorpamplona.amethyst.commons.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer @@ -97,7 +98,6 @@ import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton -import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.EndedFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveFlag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSidePanel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSidePanel.kt index 6a362fffb3..2ca840ae1e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSidePanel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSidePanel.kt @@ -49,6 +49,7 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.route_notifications import com.vitorpamplona.amethyst.commons.ui.feeds.ScrollStateKeys +import com.vitorpamplona.amethyst.commons.ui.layouts.NotificationPanelWidth import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness @@ -56,7 +57,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size12dp import com.vitorpamplona.amethyst.commons.ui.theme.Size16dp import com.vitorpamplona.amethyst.commons.ui.theme.Size22Modifier import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer -import com.vitorpamplona.amethyst.ui.layouts.NotificationPanelWidth import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt index c111567372..2f36698470 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastEpisodeListItem.kt @@ -43,13 +43,13 @@ import com.vitorpamplona.amethyst.commons.resources.podcast_episode_number import com.vitorpamplona.amethyst.commons.resources.podcast_season import com.vitorpamplona.amethyst.commons.resources.podcast_season_episode import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeAgoLabels +import com.vitorpamplona.amethyst.commons.ui.note.timeAgoWith import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.ReactionsRow -import com.vitorpamplona.amethyst.ui.note.rememberTimeAgoLabels -import com.vitorpamplona.amethyst.ui.note.timeAgoWith import com.vitorpamplona.amethyst.ui.note.types.PodcastEpisodeAudioPlayer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.podcasts.PodcastEpisode diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTrailerListItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTrailerListItem.kt index 702523008c..0cfe5c96db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTrailerListItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTrailerListItem.kt @@ -42,11 +42,11 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.podcast_trailer import com.vitorpamplona.amethyst.commons.resources.podcast_trailer_season import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeAgoLabels +import com.vitorpamplona.amethyst.commons.ui.note.timeAgoWith import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.grayText -import com.vitorpamplona.amethyst.ui.note.rememberTimeAgoLabels -import com.vitorpamplona.amethyst.ui.note.timeAgoWith import com.vitorpamplona.amethyst.ui.note.types.PodcastEpisodeAudioPlayer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nipXXPodcasting20.trailer.Podcasting20TrailerEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/results/PollResultsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/results/PollResultsScreen.kt index 05441ae449..884d0d827a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/results/PollResultsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/results/PollResultsScreen.kt @@ -94,6 +94,10 @@ import com.vitorpamplona.amethyst.commons.resources.poll_results_your_pick import com.vitorpamplona.amethyst.commons.resources.poll_single_choice import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle +import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot +import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness @@ -115,10 +119,6 @@ import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserLine import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle -import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot -import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.ui.note.types.UserGallery import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.results.datasources.PollResponsesFilterAssemblerSubscription diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt index 235be10c53..731c811b4d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt @@ -65,6 +65,7 @@ import com.vitorpamplona.amethyst.commons.resources.telegram import com.vitorpamplona.amethyst.commons.resources.twitter import com.vitorpamplona.amethyst.commons.resources.website import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.lastSeenSentence import com.vitorpamplona.amethyst.commons.ui.richtext.CreateTextWithEmoji import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size15Modifier @@ -78,7 +79,6 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.util.LongPressCopyText import com.vitorpamplona.amethyst.ui.note.DrawPlayName import com.vitorpamplona.amethyst.ui.note.ObserveAndRenderNIP05VerifiedSymbol -import com.vitorpamplona.amethyst.ui.note.lastSeenSentence import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.DisplayAppRecommendations diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt index b0bd3b3ed1..23d56d30ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relayauth/RelayAuthSettingsScreen.kt @@ -112,13 +112,13 @@ import com.vitorpamplona.amethyst.commons.resources.relay_info import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.timeAgo import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.MediumRelayIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.RelayIconFilter import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.relayAuthPurposeLabelRes -import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsDivider import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSwitchTile diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt index a49752d925..2d10db1d61 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt @@ -158,6 +158,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled import com.vitorpamplona.amethyst.commons.ui.components.appendLink import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder @@ -180,7 +181,6 @@ import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.graspLink import com.vitorpamplona.amethyst.ui.note.nipLink -import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSyncScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSyncScreen.kt index 1056c3988c..18b9e8c273 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSyncScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/eventsync/EventSyncScreen.kt @@ -106,11 +106,11 @@ import com.vitorpamplona.amethyst.commons.resources.event_sync_what_happens_titl import com.vitorpamplona.amethyst.commons.resources.event_sync_wifi_warning import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDotNoDay import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.ui.note.formatMediumDate -import com.vitorpamplona.amethyst.ui.note.timeAgoNoDotNoDay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/scheduledposts/ScheduledPostsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/scheduledposts/ScheduledPostsScreen.kt index 9d7eedbcfb..097bb9282c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/scheduledposts/ScheduledPostsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/scheduledposts/ScheduledPostsScreen.kt @@ -129,12 +129,12 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarSize import com.vitorpamplona.amethyst.commons.ui.note.ArrowBackIcon +import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot +import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.service.scheduledposts.ScheduledPostWorker import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar -import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot -import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt index 1651960528..2faf472a8c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt @@ -131,9 +131,9 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.ui.theme.contentColorOnAccent import com.vitorpamplona.amethyst.commons.ui.theme.isLight +import com.vitorpamplona.amethyst.commons.ui.theme.previewColor +import com.vitorpamplona.amethyst.commons.ui.theme.toFontFamily import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.theme.previewColor -import com.vitorpamplona.amethyst.ui.theme.toFontFamily import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt index 3459eec409..cdae46aa2a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt @@ -110,10 +110,10 @@ import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled import com.vitorpamplona.amethyst.commons.ui.components.EmptyState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.model.cordn.CordnRuntime import com.vitorpamplona.amethyst.ui.note.UserPicture -import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CopyableKeyRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSection diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46ActivityUi.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46ActivityUi.kt index 16c4a71e1e..e4180fef22 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46ActivityUi.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46ActivityUi.kt @@ -53,8 +53,8 @@ import com.vitorpamplona.amethyst.commons.resources.nip46_signer_act_ping import com.vitorpamplona.amethyst.commons.resources.nip46_signer_act_shared_pubkey import com.vitorpamplona.amethyst.commons.resources.nip46_signer_act_signed_kind import com.vitorpamplona.amethyst.commons.resources.nip46_signer_activity_denied +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo private val LiveGreen = Color(0xFF3DDC84) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index fbd05a0120..3e68c643d4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -102,6 +102,8 @@ import com.vitorpamplona.amethyst.commons.ui.note.RenderCashuMint import com.vitorpamplona.amethyst.commons.ui.note.RenderCodeSnippetHeaderForThread import com.vitorpamplona.amethyst.commons.ui.note.RenderFedimint import com.vitorpamplona.amethyst.commons.ui.note.RenderMintRecommendation +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness @@ -164,8 +166,6 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayReward import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton import com.vitorpamplona.amethyst.ui.note.elements.Reward import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgoStyle import com.vitorpamplona.amethyst.ui.note.nip22Comments.DisplayCommentScope import com.vitorpamplona.amethyst.ui.note.observeEdits import com.vitorpamplona.amethyst.ui.note.showAmount diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/UserCardHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/UserCardHeader.kt index 62b3f8d31a..a1a1076c97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/UserCardHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/UserCardHeader.kt @@ -33,12 +33,12 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.navigation.routeFor import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton -import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 5521b8f719..d10655579f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -21,98 +21,31 @@ package com.vitorpamplona.amethyst.ui.theme import android.app.Activity -import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.ColorScheme -import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.unit.Density import androidx.core.view.WindowCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.patrykandpatrick.vico.compose.common.VicoTheme import com.patrykandpatrick.vico.compose.common.VicoTheme.CandlestickCartesianLayerColors import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.commons.icons.symbols.ProvideAppIcons import com.vitorpamplona.amethyst.commons.model.AccentColorType import com.vitorpamplona.amethyst.commons.model.FontFamilyType import com.vitorpamplona.amethyst.commons.model.FontSizeType import com.vitorpamplona.amethyst.commons.model.ThemeType -import com.vitorpamplona.amethyst.commons.ui.components.LocalAnimationsEnabled -import com.vitorpamplona.amethyst.commons.ui.components.LocalProfilePictureCache import com.vitorpamplona.amethyst.commons.ui.screen.DisplaySettings -import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.screen.collectDisplaySettings -import com.vitorpamplona.amethyst.commons.ui.theme.AccentBlueDark -import com.vitorpamplona.amethyst.commons.ui.theme.AccentBlueLight -import com.vitorpamplona.amethyst.commons.ui.theme.AccentGreenDark -import com.vitorpamplona.amethyst.commons.ui.theme.AccentGreenLight -import com.vitorpamplona.amethyst.commons.ui.theme.AccentOrangeDark -import com.vitorpamplona.amethyst.commons.ui.theme.AccentOrangeLight -import com.vitorpamplona.amethyst.commons.ui.theme.AccentPinkDark -import com.vitorpamplona.amethyst.commons.ui.theme.AccentPinkLight -import com.vitorpamplona.amethyst.commons.ui.theme.AccentRedDark -import com.vitorpamplona.amethyst.commons.ui.theme.AccentRedLight -import com.vitorpamplona.amethyst.commons.ui.theme.Purple200 -import com.vitorpamplona.amethyst.commons.ui.theme.Purple500 -import com.vitorpamplona.amethyst.commons.ui.theme.Shapes -import com.vitorpamplona.amethyst.commons.ui.theme.Teal200 -import com.vitorpamplona.amethyst.commons.ui.theme.Typography -import com.vitorpamplona.amethyst.commons.ui.theme.amethystDarkColorScheme -import com.vitorpamplona.amethyst.commons.ui.theme.amethystLightColorScheme +import com.vitorpamplona.amethyst.commons.ui.theme.AmethystMaterialTheme +import com.vitorpamplona.amethyst.commons.ui.theme.amethystDarkColors +import com.vitorpamplona.amethyst.commons.ui.theme.amethystLightColors +import com.vitorpamplona.amethyst.commons.ui.theme.isDarkTheme import com.vitorpamplona.amethyst.commons.ui.theme.isLight import com.vitorpamplona.amethyst.commons.ui.theme.transparentBackground -import com.vitorpamplona.amethyst.commons.ui.theme.withFontFamily - -// The accent color (primary/secondary/tertiary) is user-selectable in Settings -> Accent Color. -// Purple keeps the original Amethyst look (purple primary + teal secondary). Every other accent -// uses its single hue across primary and secondary for a cohesive single-color theme. -private fun accentPrimary( - accent: AccentColorType, - dark: Boolean, -): Color = - when (accent) { - AccentColorType.PURPLE -> if (dark) Purple200 else Purple500 - AccentColorType.BLUE -> if (dark) AccentBlueDark else AccentBlueLight - AccentColorType.GREEN -> if (dark) AccentGreenDark else AccentGreenLight - AccentColorType.ORANGE -> if (dark) AccentOrangeDark else AccentOrangeLight - AccentColorType.RED -> if (dark) AccentRedDark else AccentRedLight - AccentColorType.PINK -> if (dark) AccentPinkDark else AccentPinkLight - } - -private fun accentSecondary( - accent: AccentColorType, - dark: Boolean, -): Color = if (accent == AccentColorType.PURPLE) Teal200 else accentPrimary(accent, dark) - -// Representative colour for an accent option, used by the Settings accent-picker swatches — the -// same primary the theme would apply for the given light/dark mode, so the swatch previews the -// real result. -fun AccentColorType.previewColor(dark: Boolean): Color = accentPrimary(this, dark) - -private fun darkColors(accent: AccentColorType): ColorScheme = - amethystDarkColorScheme( - primary = accentPrimary(accent, dark = true), - secondary = accentSecondary(accent, dark = true), - inversePrimary = accentPrimary(accent, dark = false), - ) - -private fun lightColors(accent: AccentColorType): ColorScheme = - amethystLightColorScheme( - primary = accentPrimary(accent, dark = false), - secondary = accentSecondary(accent, dark = false), - inversePrimary = accentPrimary(accent, dark = true), - ) val chartLightColors = VicoTheme( @@ -155,6 +88,10 @@ fun AmethystTheme(content: @Composable () -> Unit) { AmethystTheme(theme, accentColor, fontFamily, fontSize, displaySettings, content) } +/** + * The shared [AmethystMaterialTheme] plus what only Android has: the system status and + * navigation bars, tinted to match. + */ @Composable fun AmethystTheme( prefTheme: ThemeType, @@ -168,44 +105,22 @@ fun AmethystTheme( // MODIFY_DAY_NIGHT_MODE, which this app does not declare, so the call silently no-ops — and it // ran on every recomposition of the theme, writing device state from inside composition. The // in-app choice is applied through the colour scheme below, which is what actually took effect. - val darkTheme = - when (prefTheme) { - ThemeType.DARK -> true - ThemeType.LIGHT -> false - else -> isSystemInDarkTheme() - } + val darkTheme = isDarkTheme(prefTheme) val colors = remember(darkTheme, accentColor) { - if (darkTheme) darkColors(accentColor) else lightColors(accentColor) + if (darkTheme) amethystDarkColors(accentColor) else amethystLightColors(accentColor) } - val resolvedFontFamily = remember(fontFamily) { fontFamily.toFontFamily() } - val typography = remember(fontFamily) { Typography.withFontFamily(resolvedFontFamily) } - - val density = LocalDensity.current - val scaledDensity = - remember(density, fontSize) { - Density(density.density, density.fontScale * fontSize.scale) - } - - MaterialTheme( - colorScheme = colors, - typography = typography, - shapes = Shapes, - content = { - ProvideAppIcons { - CompositionLocalProvider( - LocalDensity provides scaledDensity, - // ImageLoaderSetup registers the avatar thumbnail cache and the local Blossom bridge. - LocalProfilePictureCache provides true, - LocalDisplaySettings provides displaySettings, - // Performance mode turns decorative animations (crossfades) off app-wide. - LocalAnimationsEnabled provides !displaySettings.performanceMode, - LocalTextStyle provides LocalTextStyle.current.merge(TextStyle(fontFamily = resolvedFontFamily)), - content = content, - ) - } - }, + AmethystMaterialTheme( + darkTheme = darkTheme, + accentColor = accentColor, + fontFamily = fontFamily, + fontSize = fontSize, + displaySettings = displaySettings, + // ImageLoaderSetup registers the avatar thumbnail cache and the local Blossom bridge. + profilePictureCache = true, + colors = colors, + content = content, ) val view = LocalView.current @@ -226,13 +141,3 @@ fun AmethystTheme( } } } - -// Maps the user-selected font preference to a Compose [FontFamily]. -// SYSTEM returns null so the platform default is used unchanged. -fun FontFamilyType.toFontFamily(): FontFamily? = - when (this) { - FontFamilyType.SYSTEM -> null - FontFamilyType.SANS_SERIF -> FontFamily.SansSerif - FontFamilyType.SERIF -> FontFamily.Serif - FontFamilyType.MONOSPACE -> FontFamily.Monospace - } diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt index 46c164ae5d..d6600bd914 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt @@ -21,6 +21,8 @@ package com.vitorpamplona.amethyst.ui.layouts import com.vitorpamplona.amethyst.commons.ui.layouts.NavigationStyle +import com.vitorpamplona.amethyst.commons.ui.layouts.decideNavigationStyle +import com.vitorpamplona.amethyst.commons.ui.layouts.hasRoomForNotificationPanel import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue diff --git a/commons/plans/2026-08-30-commons-migration-sweep.md b/commons/plans/2026-08-30-commons-migration-sweep.md index 46de62e8af..35693ab7e1 100644 --- a/commons/plans/2026-08-30-commons-migration-sweep.md +++ b/commons/plans/2026-08-30-commons-migration-sweep.md @@ -1432,3 +1432,72 @@ functional bugs. The follow-ups: Java ignored, which no browser encoder produces. `%1$d` arguments now render ASCII digits in every locale, the same as every other migrated string. `ChannelFeedContentState` reaches `LocalCache.appHost` for its main-thread check. + +## 2026-09-27 — the rest of the single-blocker UI, `TimeAgo`, and the theme + +### Moved + +| What | To | The blocker, and how it went | +|---|---|---| +| `ScreenLayout` tier logic, `CappedScreenContent`, the pane widths | `commonsUI/…/ui/layouts/ScreenLayout.kt` | `material3-window-size-class` (app-only): its width breakpoints (Medium ≥ 600dp, Expanded ≥ 840dp) are inlined, and `ScreenLayoutTest`'s 17 cases pass against them. `LocalConfiguration` stays in the app, which asks the shared `rememberScreenLayoutSpec(widthDp, heightDp)`, so Desktop can supply its own window size. | +| `animatedViewerChromeInset`, `rememberViewerControlsVisibility` | `commonsUI/…/ui/components/ViewerChrome.kt` | They sat in a file full of Android window code. Only `ImmersiveSystemBarsEffect` (Window/insets controller) stays. | +| `SlidingCarousel` | `commonsUI/…/ui/components` | needed the inset above | +| `AudioWaveformReadOnly` | `commonsUI/…/ui/components` | two enums from the Android-only audiowaveform library, now local enums with the same values | +| `FileAttachmentCard` (+ `FileAttachmentRow`, now public) | `commonsUI/…/ui/components` | `extractFilename` hoisted to `commons/…/util/MimeTypeLabels.kt` | +| `PdfFetcher` | `commonsUI/src/jvmAndroid/…/service/pdf` | `Amethyst.instance.diskCache` → a `diskCache` parameter. commonsUI jvmAndroid gained `okhttp-coroutines` (already on every module that ships OkHttp). | +| `TimeAgoFormatter` (`timeAgo*`, `timeAbsolute*`, `dateFormatter`, `lastSeenSentence`, `timeAgoShort`, `TimeAgoLabels`) | `commonsUI/…/ui/note/TimeAgoFormatter.kt` | `android.text.format` — see below | +| `ToggleableTimeAgoText`, `TimeAgo`, `NormalTimeAgo`, `TimeAgoStyle`, `NowProvider`, `LocalNowSeconds` | `commonsUI/…/ui/note/elements` | followed the formatter | +| `AmethystTheme`'s scheme, typography and providers | `commonsUI/…/ui/theme/AmethystTheme.kt` (`AmethystMaterialTheme`, `amethystDark/LightColors`, `isDarkTheme`, `previewColor`, `toFontFamily`) | The app's `AmethystTheme` resolves the prefs, calls it, then tints the system bars. The Vico chart colours stay app-side (Vico is Android-only here). | + +**The date-format seam.** `PlatformDateFormat.kt` (commonsUI) has four expects: + +- `DateSkeletonFormatter(skeleton)`: + - Android: `getBestDateTimePattern` in a `ThreadLocal` `SimpleDateFormat`, exactly the old code. + - JVM: `DateTimeFormatter.ofLocalizedPattern` (JDK 19+, the same CLDR skeleton lookup), cached per locale and zone. + - iOS: `NSDateFormatter.setLocalizedDateFormatFromTemplate`. +- `calendarYearAndDay`: `Calendar` on jvmAndroid, `NSCalendar` on iOS. +- `rememberTimeOfDayFormatter`: + - Android: `DateFormat.getTimeFormat(context)` per call, as before, so the system 12/24-hour setting is still followed. + - JVM: the locale's SHORT time. + - iOS: `NSDateFormatterShortStyle`. +- `relativeTimeSpanShort`: + - Android: `DateUtils`, as before. + - Elsewhere: the compact "5m" form. + +`DateSkeletonFormatterTest` pins the JVM side: en-US vs en-GB order from the same instance after a locale switch, the three skeletons, day/year boundaries, and the same-day branch of `timeAbsoluteWith`. + +Desktop still has its own `ToggleableTimeAgoText` and the older `commons/…/util/TimeAgoFormatter.kt` (hard-coded English units, `DateFormat.MEDIUM`). Merging those onto this one is the Desktop phase. + +### What is left in `amethyst/ui`, measured (2026-09-27) + +A transitive-blocker sweep of the 1,385 files under `amethyst/…/ui/`, after this round: + +- **80 files have no blocker left.** Many are headless and belong in `commons`, not `commonsUI`: + - filter assemblers and `*LastRead`; + - `NewMessageTagger`, `SplitConversor`, `PubKeyFormatter`, `SettingsCatalog`; + - the Tor status/dialog VM; + - `ChatBubbleLayout` + `ChatGroupPosition` + `JumboEmoji` + `NewDateOrSubjectDivisor` + `AutoScrollToNewest`. + - The chat bubble set is the next obvious batch: Desktop's `ui/chats/ChatBubbleLayout.kt` is an older fork of it. +- **`AccountViewModel` is the wall:** + - 892 files touch it, and 310 touch nothing else app-side. Yet swapping it for an interface frees only 76 files by itself, because the rest call hub composables that are blocked themselves. + - The ui files use 210 distinct members of it. The note renderers use 28. + - Only ~10 note renderers become movable with a context interface alone. +- **The real levers are about ten hub composables**, each blocking the ui files that call it: + + | Hub | Files blocked | + |---|---| + | `UserProfilePicture` | 123 | + | `RouteMaker` | 89 | + | `UsernameDisplay` | 77 | + | `Loaders` | 66 | + | `DisappearingScaffold` | 64 (AVM only, 53 lines) | + | `RichTextViewer` | 62 | + | `NoteCompose` | 58 | + | `FeedContentStateView` / `FeedView` | AVM only | + + Also on the list: + - the `reqCommand` `observe*` helpers (176 files; they take `accountViewModel` themselves); + - the flavour-only `TranslatableRichTextViewer` (54 files), which wants a slot or a CompositionLocal. +- **Two corrections to the MOVE-AFTER table above:** + - `INav`/`Route` are no longer blockers: every ui file imports the commons ones. + - `ui/note/types` is 111 files, not ~89, and "AVM threading" understates it: the hubs matter more than the parameter. diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/MimeTypeLabels.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/MimeTypeLabels.kt index 1911a22019..5d1f7c8fd1 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/MimeTypeLabels.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/MimeTypeLabels.kt @@ -50,3 +50,10 @@ fun prettyMime(mime: String): String = "application/webbundle" -> "Web Bundle" else -> mime } + +/** The last path segment of [url], without query or fragment; the whole [url] when that is blank. */ +fun extractFilename(url: String): String { + val afterQuery = url.substringBefore('?').substringBefore('#') + val name = afterQuery.substringAfterLast('/', afterQuery) + return if (name.isBlank()) url else name +} diff --git a/commonsUI/build.gradle.kts b/commonsUI/build.gradle.kts index 4d54405f96..87e9b9a0ea 100644 --- a/commonsUI/build.gradle.kts +++ b/commonsUI/build.gradle.kts @@ -160,6 +160,8 @@ kotlin { // OkHttp for the Blossom read-auth Coil fetcher. implementation(libs.okhttp) + // executeAsync for PdfFetcher; already on every module that ships OkHttp. + implementation(libs.okhttpCoroutines) // Markdown rendering (richtext-commonmark). The single // consumer (RenderMarkdown.kt) lives in jvmAndroid. diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt new file mode 100644 index 0000000000..1aa0e916ea --- /dev/null +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt @@ -0,0 +1,78 @@ +/* + * 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.commons.ui.note + +import android.text.format.DateFormat +import android.text.format.DateUtils +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * Per-thread cached [SimpleDateFormat] keyed off the current default [Locale]. + * + * `SimpleDateFormat` is mutable and not thread-safe, and these formatters are read from both + * the UI thread (composition) and background coroutines. `ThreadLocal` gives each thread its + * own instance: no locks, no allocation per call, and a lazy rebuild on locale change. + */ +actual class DateSkeletonFormatter actual constructor( + private val skeleton: String, +) { + private val cache = ThreadLocal<Pair<Locale, SimpleDateFormat>>() + + private fun get(): SimpleDateFormat { + val current = Locale.getDefault() + val cached = cache.get() + if (cached != null && cached.first == current) return cached.second + val fresh = SimpleDateFormat(DateFormat.getBestDateTimePattern(current, skeleton), current) + cache.set(current to fresh) + return fresh + } + + actual fun format(epochMillis: Long): String = get().format(epochMillis) +} + +/** Built per call, as before the move: the system 12/24-hour setting can change under a running app. */ +@Composable +actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String { + val context = LocalContext.current + return remember(context) { { epochMillis -> DateFormat.getTimeFormat(context).format(Date(epochMillis)) } } +} + +actual fun relativeTimeSpanShort( + epochMillis: Long, + nowMillis: Long, + nowLabel: String, + fallback: () -> String, +): String { + val humanReadable = + DateUtils + .getRelativeTimeSpanString( + epochMillis, + nowMillis, + DateUtils.MINUTE_IN_MILLIS, + DateUtils.FORMAT_ABBREV_ALL, + ).toString() + return if (humanReadable.startsWith("In") || humanReadable.startsWith("0")) nowLabel else humanReadable +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AudioWaveformReadOnly.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/AudioWaveformReadOnly.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AudioWaveformReadOnly.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/AudioWaveformReadOnly.kt index ea26b2582a..712a76b999 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AudioWaveformReadOnly.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/AudioWaveformReadOnly.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.components +package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.animateFloatAsState @@ -45,8 +45,6 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.coerceIn import androidx.compose.ui.unit.dp -import com.linc.audiowaveform.model.AmplitudeType -import com.linc.audiowaveform.model.WaveformAlignment import kotlin.math.ceil import kotlin.math.roundToInt @@ -204,3 +202,9 @@ internal fun Iterable<Float>.normalize( } private fun Int.safeDiv(value: Int): Float = if (value == 0) 0F else this / value.toFloat() + +/** Where each bar sits in the row. Same values as the audiowaveform library's enum, which is Android-only. */ +enum class WaveformAlignment { Top, Center, Bottom } + +/** How a bucket of samples becomes one bar. Same values as the audiowaveform library's enum. */ +enum class AmplitudeType { Avg, Min, Max } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/FileAttachmentCard.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileAttachmentCard.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/FileAttachmentCard.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileAttachmentCard.kt index 07bd4f82be..5960bb8db3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/FileAttachmentCard.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileAttachmentCard.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.components +package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -44,8 +44,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.MaxWidthWithHorzPadding import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.commons.ui.theme.innerPostModifier import com.vitorpamplona.amethyst.commons.util.countToHumanReadableBytes +import com.vitorpamplona.amethyst.commons.util.extractFilename import com.vitorpamplona.amethyst.commons.util.prettyMime -import com.vitorpamplona.amethyst.ui.components.pdf.extractFilename /** * The renderer for a declared file that none of the media viewers can display — a webxdc app, @@ -94,7 +94,7 @@ fun FileAttachmentCard( * [com.vitorpamplona.amethyst.ui.components.pdf.PdfPreviewCard]. */ @Composable -internal fun FileAttachmentRow( +fun FileAttachmentRow( symbol: MaterialSymbol, title: String, subtitle: String?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SlidingCarousel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/SlidingCarousel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SlidingCarousel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/SlidingCarousel.kt index ea7976700c..6af0b5d492 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SlidingCarousel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/SlidingCarousel.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.components +package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ViewerChrome.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ViewerChrome.kt new file mode 100644 index 0000000000..c93a2283c7 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/ViewerChrome.kt @@ -0,0 +1,130 @@ +/* + * 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.commons.ui.components + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.systemBars +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeoutOrNull + +// Chrome shared by the full-screen media viewers and the carousels inside them: how the +// controls auto-hide, and how far they sit from a screen edge. The platform half (hiding +// the OS bars) stays with each front end's viewer. + +// Opening churn -- insets arriving, then the bars being hidden -- must not look like a user +// gesture, so the row snaps through it and only animates afterwards. +private const val CONTROLS_SETTLE_BEFORE_ANIMATING_MS = 350L + +// Roughly the system bars' own show/hide duration, so the row travels with them rather than +// trailing after they have already arrived. +private const val CONTROLS_SLIDE_MS = 200 + +// Keeps the row off the screen edge -- and off the rounded corners -- while the bars are hidden. +private val VIEWER_CHROME_EDGE_GAP = 16.dp + +// How long the controls stay up before the viewer fades them out on its own. +private const val CONTROLS_AUTO_HIDE_DELAY_MS = 2000L + +/** + * Visibility of the viewer controls: they start on screen, fade out on their own after + * [CONTROLS_AUTO_HIDE_DELAY_MS], and the caller flips the returned state on tap. + * + * [holdOpen] freezes the timer while something anchored to the controls -- the share sheet, say -- + * is up, and re-arms it once that closes. [armed] withholds the countdown until there is something + * to look at, so a viewer that spends three seconds fetching its media doesn't reveal the first + * frame with the controls already gone. + * + * A tap that brings the controls back deliberately gets no timer: the user asked for them, so they + * stay until tapped away. That is why the countdown races the controls going away rather than just + * sleeping -- a timer left over from an earlier show would otherwise wipe controls the user tapped + * back up in the meantime. + */ +@Composable +fun rememberViewerControlsVisibility( + holdOpen: Boolean, + armed: Boolean = true, +): MutableState<Boolean> { + val visible = remember { mutableStateOf(true) } + + LaunchedEffect(armed, holdOpen) { + if (!armed || holdOpen) return@LaunchedEffect + + val hiddenFirst = + withTimeoutOrNull(CONTROLS_AUTO_HIDE_DELAY_MS) { + snapshotFlow { visible.value }.first { !it } + } + + if (hiddenFirst == null) visible.value = false + } + + return visible +} + +/** + * How far the viewer chrome sits from a screen edge: the system bar's own height while the bar is + * on screen, and a thin constant once it is hidden -- so the chrome follows the bar instead of + * reserving space for one that is not there. + * + * This only works because the viewer asks for BEHAVIOR_DEFAULT rather than transient bars. Under + * BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE a peeked bar is painted over the content and dispatches no + * insets at all -- `systemBars` stays 0 and `isVisible` stays false the whole time it is on screen + * -- so nothing here could react to it. + * + * The value is animated, but snapped for [CONTROLS_SETTLE_BEFORE_ANIMATING_MS] after the chrome + * appears. Opening moves the inset twice for reasons the user did not cause: the window has not + * been told its insets yet (they read 0), and ImmersiveSystemBarsEffect hides the bars from a + * DisposableEffect that runs after composition. Animating either would play a slide on open. + */ +@Composable +fun animatedViewerChromeInset(atBottom: Boolean): Dp { + val density = LocalDensity.current + val bars = WindowInsets.systemBars + val barPx = if (atBottom) bars.getBottom(density) else bars.getTop(density) + val target = with(density) { maxOf(barPx, VIEWER_CHROME_EDGE_GAP.roundToPx()).toDp() } + + var settled by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + delay(CONTROLS_SETTLE_BEFORE_ANIMATING_MS) + settled = true + } + + val animated by animateDpAsState( + targetValue = target, + animationSpec = if (settled) tween(durationMillis = CONTROLS_SLIDE_MS) else snap(), + label = "viewerChromeInset", + ) + return animated +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/ScreenLayout.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/ScreenLayout.kt index 03fb009018..260e228a53 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/ScreenLayout.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/ScreenLayout.kt @@ -20,8 +20,18 @@ */ package com.vitorpamplona.amethyst.commons.ui.layouts +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp /** How the app shell presents its top-level navigation for the current window size. */ enum class NavigationStyle { @@ -60,3 +70,114 @@ data class ScreenLayoutSpec( } val LocalScreenLayout = compositionLocalOf { ScreenLayoutSpec.Phone } + +/** + * Material 3's window width classes: Compact below 600dp, Medium below 840dp, Expanded from + * there. Inlined rather than taken from `material3-window-size-class`, which only the Android + * app depends on. + */ +private const val MEDIUM_WIDTH_DP = 600 +private const val EXPANDED_WIDTH_DP = 840 + +/** + * Minimum window width for the docked notification panel: a leading navigation pane, a + * readable center pane and the panel ([NotificationPanelWidth]) only coexist comfortably from + * a landscape-tablet-sized window up. Sized against the widest leading pane, the permanent + * drawer ([PermanentDrawerWidth]); the rail is narrower, so a railed window that clears this + * gets a roomier center pane rather than a tighter one. + */ +private const val NOTIFICATION_PANEL_MIN_WINDOW_DP = 1200 + +val PermanentDrawerWidth = 300.dp + +val NotificationPanelWidth = 360.dp + +/** + * Maximum width of a screen's content column inside a wide center pane. Every NavHost + * destination is wrapped in [CappedScreenContent] (via the builders in NavigationEffects), + * so the whole screen — top bar, tabs, feed, settings rows — shares one centered reading + * column instead of stretching across the pane. Screens that genuinely need the full pane + * (Messages' two-pane split, the embedded browser surfaces) opt out at registration. + */ +val FeedContentMaxWidth = 600.dp + +/** + * Minimum window height for the docked drawer. Higher than Material's 480dp Compact/Medium + * height boundary on purpose: the permanent drawer's own header — banner, avatar, status + * editor, follower counts — fills most of a ~540dp column before the first navigation row, so + * below this the rail shows more of the menu than the dock does. + */ +private const val DOCK_MIN_WINDOW_HEIGHT_DP = 600 + +/** + * The navigation tier for a window of this shape. + * + * The dock is not a width decision. A tablet is past the Expanded breakpoint in both + * orientations, so keying on width alone pins 300dp of menu open in portrait with no closed + * state to fall back on (issue #4024). It docks only when the window is wide, landscape, and + * tall enough for the drawer's own content to be usable; everything else that is not Compact + * falls through to the rail, which pairs with the existing swipe-in modal drawer. + * + * A square window counts as landscape and docks; `Configuration.ORIENTATION_LANDSCAPE` + * breaks that tie the other way, so the two disagree at exactly width == height. + */ +fun decideNavigationStyle( + windowWidthDp: Int, + windowHeightDp: Int, +): NavigationStyle = + when { + windowWidthDp >= EXPANDED_WIDTH_DP && + windowWidthDp >= windowHeightDp && + windowHeightDp >= DOCK_MIN_WINDOW_HEIGHT_DP -> NavigationStyle.PERMANENT_DRAWER + windowWidthDp >= MEDIUM_WIDTH_DP -> NavigationStyle.NAV_RAIL + else -> NavigationStyle.BOTTOM_BAR + } + +/** + * Whether the window is wide enough to dock the notification feed beside the content. + * + * Deliberately not keyed on [NavigationStyle]: a wide portrait window now gets the rail, and + * gating on the dock would strip a panel it has today. + */ +fun hasRoomForNotificationPanel(windowWidthDp: Int): Boolean = windowWidthDp >= NOTIFICATION_PANEL_MIN_WINDOW_DP + +/** + * Centers a destination's content at [FeedContentMaxWidth]. The outer box paints the theme + * background so the gutters match the screens' own surfaces; on Compact windows the cap is + * wider than the pane and this is a visual no-op. + */ +@Composable +fun CappedScreenContent(content: @Composable () -> Unit) { + Box( + modifier = + Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + contentAlignment = Alignment.TopCenter, + ) { + Box( + Modifier + .widthIn(max = FeedContentMaxWidth) + .fillMaxSize(), + ) { + content() + } + } +} + +/** + * The layout tier for a window of [windowWidthDp] x [windowHeightDp]. Each front end reads + * its own window size (Android: `Configuration.screenWidthDp`) and asks here, so the tier + * rule lives in one place. + */ +@Composable +fun rememberScreenLayoutSpec( + windowWidthDp: Int, + windowHeightDp: Int, +): ScreenLayoutSpec = + remember(windowWidthDp, windowHeightDp) { + ScreenLayoutSpec( + navigationStyle = decideNavigationStyle(windowWidthDp, windowHeightDp), + hasRoomForNotificationPanel = hasRoomForNotificationPanel(windowWidthDp), + ) + } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt new file mode 100644 index 0000000000..36bfd747a6 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt @@ -0,0 +1,56 @@ +/* + * 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.commons.ui.note + +import androidx.compose.runtime.Composable + +/** + * Formats instants with a Unicode LDML date skeleton ("yMMMd", "MMMd", "yMMM"), letting the + * platform pick the default locale's field order ("MMM d, y" in en-US, "d MMM y" in en-GB). + * Safe to call from any thread. Rebuilds itself when the default locale changes. + */ +expect class DateSkeletonFormatter( + skeleton: String, +) { + fun format(epochMillis: Long): String +} + +/** The instant's calendar year and day-of-year in the default time zone, as year * 1000 + day. */ +expect fun calendarYearAndDay(epochMillis: Long): Int + +/** + * A time-of-day formatter ("14:32" / "2:32 PM") that follows the platform's own 12/24-hour + * preference, which on Android is a system setting rather than a locale property. + */ +@Composable +expect fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String + +/** + * The platform's abbreviated relative span ("5 min. ago", "Yesterday"), [nowLabel] for an + * instant that rounds to now or lies ahead, and [fallback] where the platform has no such + * formatter. + */ +expect fun relativeTimeSpanShort( + epochMillis: Long, + nowMillis: Long, + nowLabel: String, + fallback: () -> String, +): String diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/TimeAgoFormatter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/TimeAgoFormatter.kt similarity index 75% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/TimeAgoFormatter.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/TimeAgoFormatter.kt index 455c2bedb0..e0a8137456 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/TimeAgoFormatter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/TimeAgoFormatter.kt @@ -18,11 +18,8 @@ * 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 +package com.vitorpamplona.amethyst.commons.ui.note -import android.content.Context -import android.text.format.DateFormat -import android.text.format.DateUtils import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember @@ -46,52 +43,23 @@ import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.quartz.utils.TimeUtils import org.jetbrains.compose.resources.StringResource -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Date -import java.util.Locale import kotlin.math.round -// Skeletons follow Unicode LDML — DateFormat.getBestDateTimePattern picks the -// correct locale-specific ordering (e.g. "MMM d, y" in en-US vs "d MMM y" in en-GB). +// Skeletons follow Unicode LDML: the platform picks the locale-specific ordering +// (e.g. "MMM d, y" in en-US vs "d MMM y" in en-GB). See [DateSkeletonFormatter]. private const val YEAR_SKELETON = "yMMMd" private const val MONTH_SKELETON = "MMMd" private const val YEAR_NO_DAY_SKELETON = "yMMM" -/** - * Per-thread cached [SimpleDateFormat] keyed off the current default [Locale]. - * - * `SimpleDateFormat` is mutable and not thread-safe, and these formatters are - * read from both the UI thread (composition) and background coroutines - * (e.g. `LocalCache.justVerify` logging failed event verifications). A bare - * `var` shared across threads would race on the formatter's internal Calendar. - * Using `ThreadLocal` gives each thread its own instance — no locks, no - * allocation per call, and we rebuild lazily on locale change. - */ -private class LocaleAwareFormatter( - private val skeleton: String, -) { - private val cache = ThreadLocal<Pair<Locale, SimpleDateFormat>>() - - fun get(): SimpleDateFormat { - val current = Locale.getDefault() - val cached = cache.get() - if (cached != null && cached.first == current) return cached.second - val fresh = SimpleDateFormat(DateFormat.getBestDateTimePattern(current, skeleton), current) - cache.set(current to fresh) - return fresh - } -} - -private val yearFormatter = LocaleAwareFormatter(YEAR_SKELETON) -private val monthFormatter = LocaleAwareFormatter(MONTH_SKELETON) -private val yearNoDayFormatter = LocaleAwareFormatter(YEAR_NO_DAY_SKELETON) +private val yearFormatter = DateSkeletonFormatter(YEAR_SKELETON) +private val monthFormatter = DateSkeletonFormatter(MONTH_SKELETON) +private val yearNoDayFormatter = DateSkeletonFormatter(YEAR_NO_DAY_SKELETON) /** * The handful of unit labels the relative formatters splice into their output. * * Resolved once in composition so the formatters themselves can stay ordinary - * functions. [com.vitorpamplona.amethyst.ui.note.elements.TimeAgo] builds its text + * functions. [com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo] builds its text * inside a `derivedStateOf`, which is not a composable scope, so a @Composable * formatter could not be called from there at all. */ @@ -133,8 +101,8 @@ fun timeAgoWith( val timeDifference = TimeUtils.now() - time return when { - timeDifference > TimeUtils.ONE_YEAR -> prefix + yearFormatter.get().format(time * 1000) - timeDifference > TimeUtils.ONE_MONTH -> prefix + monthFormatter.get().format(time * 1000) + timeDifference > TimeUtils.ONE_YEAR -> prefix + yearFormatter.format(time * 1000) + timeDifference > TimeUtils.ONE_MONTH -> prefix + monthFormatter.format(time * 1000) timeDifference > TimeUtils.ONE_DAY -> prefix + (timeDifference / TimeUtils.ONE_DAY).toString() + labels.days timeDifference > TimeUtils.ONE_HOUR -> prefix + (timeDifference / TimeUtils.ONE_HOUR).toString() + labels.hours timeDifference > TimeUtils.ONE_MINUTE -> prefix + (timeDifference / TimeUtils.ONE_MINUTE).toString() + labels.minutes @@ -142,10 +110,13 @@ fun timeAgoWith( } } -/** Plain-function core of [timeAbsolute], callable outside composition. */ +/** + * Plain-function core of [timeAbsolute], callable outside composition. [timeOfDay] comes + * from [rememberTimeOfDayFormatter]. + */ fun timeAbsoluteWith( time: Long?, - context: Context, + timeOfDay: (epochMillis: Long) -> String, never: String, prefix: String = " • ", ): String { @@ -153,43 +124,39 @@ fun timeAbsoluteWith( if (time == 0L) return prefix + never val timeMs = time * 1000 - val now = Calendar.getInstance() - val then = Calendar.getInstance().apply { timeInMillis = timeMs } + val now = calendarYearAndDay(TimeUtils.nowMillis()) + val then = calendarYearAndDay(timeMs) - val sameYear = now.get(Calendar.YEAR) == then.get(Calendar.YEAR) - val sameDay = sameYear && now.get(Calendar.DAY_OF_YEAR) == then.get(Calendar.DAY_OF_YEAR) + val sameYear = now / 1000 == then / 1000 + val sameDay = now == then - val timeOfDay = DateFormat.getTimeFormat(context).format(Date(timeMs)) + val timeOfDay = timeOfDay(timeMs) return when { sameDay -> prefix + timeOfDay - sameYear -> prefix + monthFormatter.get().format(timeMs) + ", " + timeOfDay - else -> prefix + yearFormatter.get().format(timeMs) + sameYear -> prefix + monthFormatter.format(timeMs) + ", " + timeOfDay + else -> prefix + yearFormatter.format(timeMs) } } /** * Formats a Unix timestamp (seconds) as an absolute date/time string, picking the * granularity from how far away the timestamp is: - * - same day → time only (locale + system 12/24-hr aware via [DateFormat.getTimeFormat]) + * - same day → time only (locale + system 12/24-hr aware via [rememberTimeOfDayFormatter]) * - same year → "Jan 5, 14:32" / "5 Jan 14:32" / "Jan 5, 2:32 PM" (locale + system aware) * - older → "Jan 5, 2024" / "5 Jan 2024" (locale aware) * - * Used by [com.vitorpamplona.amethyst.ui.note.elements.TimeAgo] when the user + * Used by [com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo] when the user * taps the relative timestamp to reveal the absolute one. */ @Composable fun timeAbsolute( time: Long?, - context: Context, prefix: String = " • ", -): String = timeAbsoluteWith(time, context, stringRes(Res.string.never), prefix) +): String = timeAbsoluteWith(time, rememberTimeOfDayFormatter(), stringRes(Res.string.never), prefix) @Composable -fun timeAbsoluteNoDot( - time: Long?, - context: Context, -): String = timeAbsolute(time, context, prefix = "") +fun timeAbsoluteNoDot(time: Long?): String = timeAbsolute(time, prefix = "") @Composable fun timeAgo( @@ -210,11 +177,11 @@ fun timeAgoNoDot(time: Long?): String { return when { timeDifference > TimeUtils.ONE_YEAR -> { - yearFormatter.get().format(time * 1000) + yearFormatter.format(time * 1000) } timeDifference > TimeUtils.ONE_MONTH -> { - monthFormatter.get().format(time * 1000) + monthFormatter.format(time * 1000) } timeDifference > TimeUtils.ONE_DAY -> { @@ -244,11 +211,11 @@ fun timeAgoNoDotNoDay(time: Long?): String { return when { timeDifference > TimeUtils.ONE_YEAR -> { - yearNoDayFormatter.get().format(time * 1000) + yearNoDayFormatter.format(time * 1000) } timeDifference > TimeUtils.ONE_MONTH -> { - monthFormatter.get().format(time * 1000) + monthFormatter.format(time * 1000) } timeDifference > TimeUtils.ONE_DAY -> { @@ -278,11 +245,11 @@ fun timeAheadNoDot(time: Long?): String { return when { timeDifference > TimeUtils.ONE_YEAR -> { - yearFormatter.get().format(time * 1000) + yearFormatter.format(time * 1000) } timeDifference > TimeUtils.ONE_MONTH -> { - monthFormatter.get().format(time * 1000) + monthFormatter.format(time * 1000) } timeDifference > TimeUtils.ONE_DAY -> { @@ -314,9 +281,9 @@ fun dateFormatter( val timeDifference = TimeUtils.now() - time return if (timeDifference > TimeUtils.ONE_YEAR) { - yearFormatter.get().format(time * 1000) + yearFormatter.format(time * 1000) } else if (timeDifference > TimeUtils.ONE_DAY) { - monthFormatter.get().format(time * 1000) + monthFormatter.format(time * 1000) } else { today } @@ -394,28 +361,19 @@ fun lastSeenSentence(time: Long?): String { } } - val dateText = yearFormatter.get().format(time * 1000) + val dateText = yearFormatter.format(time * 1000) return stringRes(Res.string.last_seen_on_date, dateText, durationText) } +/** + * "5 min. ago" style on platforms that have an abbreviated relative-span formatter (Android's + * DateUtils), the compact relative form ("5m") elsewhere. + */ fun timeAgoShort( - mills: Long?, - stringForNow: String, + time: Long?, + labels: TimeAgoLabels, ): String { - if (mills == null) return " " - - var humanReadable = - DateUtils - .getRelativeTimeSpanString( - mills * 1000, - System.currentTimeMillis(), - DateUtils.MINUTE_IN_MILLIS, - DateUtils.FORMAT_ABBREV_ALL, - ).toString() - if (humanReadable.startsWith("In") || humanReadable.startsWith("0")) { - humanReadable = stringForNow - } - - return humanReadable + if (time == null) return " " + return relativeTimeSpanShort(time * 1000, TimeUtils.nowMillis(), labels.now) { timeAgoWith(time, labels, prefix = "") } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NowProvider.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/NowProvider.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NowProvider.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/NowProvider.kt index f3e7a65ee6..4dfa5f2175 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/NowProvider.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/NowProvider.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.note.elements +package com.vitorpamplona.amethyst.commons.ui.note.elements import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/TimeAgo.kt similarity index 88% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/TimeAgo.kt index 312f8dc11f..1463dd1d88 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/TimeAgo.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/TimeAgo.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.note.elements +package com.vitorpamplona.amethyst.commons.ui.note.elements import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -33,18 +33,16 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.TextUnit import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.resources.Res -import com.vitorpamplona.amethyst.commons.resources.now -import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.note.elements.LocalNowSeconds +import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeAgoLabels +import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter +import com.vitorpamplona.amethyst.commons.ui.note.timeAbsoluteWith +import com.vitorpamplona.amethyst.commons.ui.note.timeAgoShort +import com.vitorpamplona.amethyst.commons.ui.note.timeAgoWith import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.note.rememberTimeAgoLabels -import com.vitorpamplona.amethyst.ui.note.timeAbsoluteWith -import com.vitorpamplona.amethyst.ui.note.timeAgoShort -import com.vitorpamplona.amethyst.ui.note.timeAgoWith /** * Which relative-time format to use when the timestamp is *not* toggled to absolute. @@ -94,21 +92,20 @@ fun ToggleableTimeAgoText( // chat timestamp opening the relay/delivery dialog) receives the tap instead. toggleable: Boolean = true, ) { - val context = LocalContext.current + val timeOfDay = rememberTimeOfDayFormatter() val nowState = LocalNowSeconds.current - val nowStr = stringRes(id = Res.string.now) val labels = rememberTimeAgoLabels() val interactionSource = remember { MutableInteractionSource() } var showAbsolute by remember(timestamp) { mutableStateOf(false) } val text by - remember(timestamp, context, style, nowState, nowStr, labels) { + remember(timestamp, timeOfDay, style, nowState, labels) { derivedStateOf { if (showAbsolute) { when (style) { - TimeAgoStyle.Dotted -> timeAbsoluteWith(timestamp, context, labels.never) - TimeAgoStyle.DottedTight -> timeAbsoluteWith(timestamp, context, labels.never).trimStart() - TimeAgoStyle.Short -> timeAbsoluteWith(timestamp, context, labels.never, prefix = "") + TimeAgoStyle.Dotted -> timeAbsoluteWith(timestamp, timeOfDay, labels.never) + TimeAgoStyle.DottedTight -> timeAbsoluteWith(timestamp, timeOfDay, labels.never).trimStart() + TimeAgoStyle.Short -> timeAbsoluteWith(timestamp, timeOfDay, labels.never, prefix = "") } } else { // Read nowState only when displaying a relative time, so an item @@ -117,7 +114,7 @@ fun ToggleableTimeAgoText( when (style) { TimeAgoStyle.Dotted -> timeAgoWith(timestamp, labels) TimeAgoStyle.DottedTight -> timeAgoWith(timestamp, labels).trimStart() - TimeAgoStyle.Short -> timeAgoShort(timestamp, nowStr) + TimeAgoStyle.Short -> timeAgoShort(timestamp, labels) } } } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/AmethystTheme.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/AmethystTheme.kt new file mode 100644 index 0000000000..4a988d5716 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/AmethystTheme.kt @@ -0,0 +1,149 @@ +/* + * 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.commons.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.Density +import com.vitorpamplona.amethyst.commons.icons.symbols.ProvideAppIcons +import com.vitorpamplona.amethyst.commons.model.AccentColorType +import com.vitorpamplona.amethyst.commons.model.FontFamilyType +import com.vitorpamplona.amethyst.commons.model.FontSizeType +import com.vitorpamplona.amethyst.commons.model.ThemeType +import com.vitorpamplona.amethyst.commons.ui.components.LocalAnimationsEnabled +import com.vitorpamplona.amethyst.commons.ui.components.LocalProfilePictureCache +import com.vitorpamplona.amethyst.commons.ui.screen.DisplaySettings +import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings + +// The accent color (primary/secondary/tertiary) is user-selectable in Settings -> Accent Color. +// Purple keeps the original Amethyst look (purple primary + teal secondary). Every other accent +// uses its single hue across primary and secondary for a cohesive single-color theme. +private fun accentPrimary( + accent: AccentColorType, + dark: Boolean, +): Color = + when (accent) { + AccentColorType.PURPLE -> if (dark) Purple200 else Purple500 + AccentColorType.BLUE -> if (dark) AccentBlueDark else AccentBlueLight + AccentColorType.GREEN -> if (dark) AccentGreenDark else AccentGreenLight + AccentColorType.ORANGE -> if (dark) AccentOrangeDark else AccentOrangeLight + AccentColorType.RED -> if (dark) AccentRedDark else AccentRedLight + AccentColorType.PINK -> if (dark) AccentPinkDark else AccentPinkLight + } + +private fun accentSecondary( + accent: AccentColorType, + dark: Boolean, +): Color = if (accent == AccentColorType.PURPLE) Teal200 else accentPrimary(accent, dark) + +// Representative colour for an accent option, used by the Settings accent-picker swatches — the +// same primary the theme would apply for the given light/dark mode, so the swatch previews the +// real result. +fun AccentColorType.previewColor(dark: Boolean): Color = accentPrimary(this, dark) + +fun amethystDarkColors(accent: AccentColorType): ColorScheme = + amethystDarkColorScheme( + primary = accentPrimary(accent, dark = true), + secondary = accentSecondary(accent, dark = true), + inversePrimary = accentPrimary(accent, dark = false), + ) + +fun amethystLightColors(accent: AccentColorType): ColorScheme = + amethystLightColorScheme( + primary = accentPrimary(accent, dark = false), + secondary = accentSecondary(accent, dark = false), + inversePrimary = accentPrimary(accent, dark = true), + ) + +/** Whether [prefTheme] resolves to dark, following the system for anything but an explicit choice. */ +@Composable +fun isDarkTheme(prefTheme: ThemeType): Boolean = + when (prefTheme) { + ThemeType.DARK -> true + ThemeType.LIGHT -> false + else -> isSystemInDarkTheme() + } + +/** + * Amethyst's Material theme and the app-wide composition locals, for any front end: the + * accent-derived colour scheme, the chosen font family and size, the display settings, and the + * icon font. Platform chrome (Android's system bars) is the caller's to apply. + * + * [profilePictureCache] is true where the image loader registers the avatar thumbnail cache. + */ +@Composable +fun AmethystMaterialTheme( + darkTheme: Boolean, + accentColor: AccentColorType = AccentColorType.PURPLE, + fontFamily: FontFamilyType = FontFamilyType.SYSTEM, + fontSize: FontSizeType = FontSizeType.NORMAL, + displaySettings: DisplaySettings = DisplaySettings(), + profilePictureCache: Boolean = false, + colors: ColorScheme = remember(darkTheme, accentColor) { if (darkTheme) amethystDarkColors(accentColor) else amethystLightColors(accentColor) }, + content: @Composable () -> Unit, +) { + val resolvedFontFamily = remember(fontFamily) { fontFamily.toFontFamily() } + val typography = remember(fontFamily) { Typography.withFontFamily(resolvedFontFamily) } + + val density = LocalDensity.current + val scaledDensity = + remember(density, fontSize) { + Density(density.density, density.fontScale * fontSize.scale) + } + + MaterialTheme( + colorScheme = colors, + typography = typography, + shapes = Shapes, + content = { + ProvideAppIcons { + CompositionLocalProvider( + LocalDensity provides scaledDensity, + LocalProfilePictureCache provides profilePictureCache, + LocalDisplaySettings provides displaySettings, + // Performance mode turns decorative animations (crossfades) off app-wide. + LocalAnimationsEnabled provides !displaySettings.performanceMode, + LocalTextStyle provides LocalTextStyle.current.merge(TextStyle(fontFamily = resolvedFontFamily)), + content = content, + ) + } + }, + ) +} + +// Maps the user-selected font preference to a Compose [FontFamily]. +// SYSTEM returns null so the platform default is used unchanged. +fun FontFamilyType.toFontFamily(): FontFamily? = + when (this) { + FontFamilyType.SYSTEM -> null + FontFamilyType.SANS_SERIF -> FontFamily.SansSerif + FontFamilyType.SERIF -> FontFamily.Serif + FontFamilyType.MONOSPACE -> FontFamily.Monospace + } diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt new file mode 100644 index 0000000000..060ad318f9 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt @@ -0,0 +1,76 @@ +/* + * 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.commons.ui.note + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import platform.Foundation.NSCalendar +import platform.Foundation.NSCalendarUnitDay +import platform.Foundation.NSCalendarUnitYear +import platform.Foundation.NSDate +import platform.Foundation.NSDateFormatter +import platform.Foundation.NSDateFormatterNoStyle +import platform.Foundation.NSDateFormatterShortStyle +import platform.Foundation.NSLocale +import platform.Foundation.currentLocale +import platform.Foundation.dateWithTimeIntervalSince1970 + +private fun dateOf(epochMillis: Long) = NSDate.dateWithTimeIntervalSince1970(epochMillis / 1000.0) + +/** NSDateFormatter is thread-safe for formatting on iOS 7+; the template picks the locale's order. */ +actual class DateSkeletonFormatter actual constructor( + skeleton: String, +) { + private val formatter = + NSDateFormatter().apply { + locale = NSLocale.currentLocale + setLocalizedDateFormatFromTemplate(skeleton) + } + + actual fun format(epochMillis: Long): String = formatter.stringFromDate(dateOf(epochMillis)) +} + +actual fun calendarYearAndDay(epochMillis: Long): Int { + val calendar = NSCalendar.currentCalendar + val date = dateOf(epochMillis) + val year = calendar.component(NSCalendarUnitYear, fromDate = date).toInt() + val day = calendar.ordinalityOfUnit(NSCalendarUnitDay, inUnit = NSCalendarUnitYear, forDate = date).toInt() + return year * 1000 + day +} + +@Composable +actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String = + remember { + val formatter = + NSDateFormatter().apply { + dateStyle = NSDateFormatterNoStyle + timeStyle = NSDateFormatterShortStyle + } + val format: (Long) -> String = { epochMillis -> formatter.stringFromDate(dateOf(epochMillis)) } + format + } + +actual fun relativeTimeSpanShort( + epochMillis: Long, + nowMillis: Long, + nowLabel: String, + fallback: () -> String, +): String = fallback() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfFetcher.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfFetcher.kt rename to commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt index c1fcee999a..6b562a668c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfFetcher.kt +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt @@ -18,10 +18,9 @@ * 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.pdf +package com.vitorpamplona.amethyst.commons.service.pdf import coil3.disk.DiskCache -import com.vitorpamplona.amethyst.Amethyst import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.OkHttpClient @@ -35,15 +34,15 @@ object PdfFetcher { * responsible for closing the returned snapshot; while it's open the cache entry cannot be * evicted, so the underlying file stays valid for `PdfRenderer`. * - * Reuses the Coil disk cache (`Amethyst.instance.diskCache`) so PDFs share the same LRU - * eviction and disk budget as images. + * Pass the app's Coil [diskCache] so PDFs share the same LRU eviction and disk budget as + * images. */ suspend fun fetchSnapshot( url: String, + diskCache: DiskCache, okHttpClient: (String) -> OkHttpClient, ): DiskCache.Snapshot = withContext(Dispatchers.IO) { - val diskCache = Amethyst.instance.diskCache // Covers the cache-hit fast path too, not just the download below it. openSnapshot() // contends on the global DiskLruCache lock, which Coil's cleanup pass holds across a // burst of unlink syscalls (see DeferredDeleteFileSystem) — calling it from a caller diff --git a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/CalendarDay.jvmAndroid.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/CalendarDay.jvmAndroid.kt new file mode 100644 index 0000000000..789aa61268 --- /dev/null +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/CalendarDay.jvmAndroid.kt @@ -0,0 +1,28 @@ +/* + * 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.commons.ui.note + +import java.util.Calendar + +actual fun calendarYearAndDay(epochMillis: Long): Int { + val calendar = Calendar.getInstance().apply { timeInMillis = epochMillis } + return calendar.get(Calendar.YEAR) * 1000 + calendar.get(Calendar.DAY_OF_YEAR) +} diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.jvm.kt new file mode 100644 index 0000000000..18b5273f35 --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.jvm.kt @@ -0,0 +1,74 @@ +/* + * 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.commons.ui.note + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import java.util.Locale + +/** + * [DateTimeFormatter.ofLocalizedPattern] (JDK 19+) resolves the skeleton against the locale's + * CLDR data, the JVM counterpart of Android's `getBestDateTimePattern`. The formatter is + * immutable, so one cached instance serves every thread; it is rebuilt when the default locale + * or time zone changes. + */ +actual class DateSkeletonFormatter actual constructor( + private val skeleton: String, +) { + private class Cached( + val locale: Locale, + val zone: ZoneId, + val formatter: DateTimeFormatter, + ) + + @Volatile private var cached: Cached? = null + + actual fun format(epochMillis: Long): String { + val locale = Locale.getDefault() + val zone = ZoneId.systemDefault() + val current = cached?.takeIf { it.locale == locale && it.zone == zone } + val formatter = + current?.formatter + ?: DateTimeFormatter.ofLocalizedPattern(skeleton).withLocale(locale).withZone(zone).also { + cached = Cached(locale, zone, it) + } + return formatter.format(Instant.ofEpochMilli(epochMillis)) + } +} + +@Composable +actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String = + remember { + val formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withZone(ZoneId.systemDefault()) + val format: (Long) -> String = { epochMillis -> formatter.format(Instant.ofEpochMilli(epochMillis)) } + format + } + +actual fun relativeTimeSpanShort( + epochMillis: Long, + nowMillis: Long, + nowLabel: String, + fallback: () -> String, +): String = fallback() diff --git a/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateSkeletonFormatterTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateSkeletonFormatterTest.kt new file mode 100644 index 0000000000..e95be9295e --- /dev/null +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/note/DateSkeletonFormatterTest.kt @@ -0,0 +1,97 @@ +/* + * 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.commons.ui.note + +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.time.LocalDateTime +import java.time.ZoneId +import java.util.Locale +import java.util.TimeZone + +class DateSkeletonFormatterTest { + private lateinit var savedLocale: Locale + private lateinit var savedZone: TimeZone + + @Before + fun pin() { + savedLocale = Locale.getDefault() + savedZone = TimeZone.getDefault() + TimeZone.setDefault(TimeZone.getTimeZone("UTC")) + } + + @After + fun restore() { + Locale.setDefault(savedLocale) + TimeZone.setDefault(savedZone) + } + + private fun millis( + year: Int, + month: Int, + day: Int, + hour: Int = 12, + ) = LocalDateTime + .of(year, month, day, hour, 0) + .atZone(ZoneId.of("UTC")) + .toInstant() + .toEpochMilli() + + @Test + fun skeletonFollowsTheLocalesFieldOrder() { + val formatter = DateSkeletonFormatter("yMMMd") + Locale.setDefault(Locale.US) + assertEquals("Jan 5, 2024", formatter.format(millis(2024, 1, 5))) + // Same instance, new locale: the cache must not hand back the US pattern. + Locale.setDefault(Locale.UK) + assertEquals("5 Jan 2024", formatter.format(millis(2024, 1, 5))) + } + + @Test + fun monthDayAndYearMonthSkeletons() { + Locale.setDefault(Locale.US) + assertEquals("Jan 5", DateSkeletonFormatter("MMMd").format(millis(2024, 1, 5))) + assertEquals("Jan 2024", DateSkeletonFormatter("yMMM").format(millis(2024, 1, 5))) + } + + @Test + fun calendarDayDistinguishesDaysAndYears() { + assertEquals(calendarYearAndDay(millis(2024, 3, 1, hour = 1)), calendarYearAndDay(millis(2024, 3, 1, hour = 23))) + assertTrue(calendarYearAndDay(millis(2024, 3, 1)) != calendarYearAndDay(millis(2024, 3, 2))) + assertEquals(2024, calendarYearAndDay(millis(2024, 12, 31)) / 1000) + assertEquals(366, calendarYearAndDay(millis(2024, 12, 31)) % 1000) + } + + @Test + fun absoluteTimeUsesTimeOfDayOnlyForToday() { + Locale.setDefault(Locale.US) + val nowSec = System.currentTimeMillis() / 1000 + val timeOfDay: (Long) -> String = { "T" } + assertEquals(" • T", timeAbsoluteWith(nowSec, timeOfDay, "never")) + assertEquals("never", timeAbsoluteWith(0L, timeOfDay, "never", prefix = "")) + // Two years back is always a different year: the date, with no time of day. + val old = nowSec - 2L * 366 * 24 * 3600 + assertTrue(!timeAbsoluteWith(old, timeOfDay, "never").contains("T")) + } +} From b1849e5880be81634f27742b5ca6b31ea15f7b6b Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 22:52:20 +0000 Subject: [PATCH 25/60] feat(browser): bring embedded nSites and nApplets to parity with full screen - An embedded nSite is shown as an nSite with the Tor row. Switching the route saves it and rebuilds the session, as the full-screen host relaunches. - "What it can access" is a new AccessInfoSheet card in both the embedded tab and the full-screen window, replacing the platform dialogs. - The embedded tab gets find in page, text size and the developer console through new NappletEmbedContract messages served by NappletHostService. Also removes nappletHost strings left unused by the Compose chrome. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TEkj7Eo2xbidVHAoF8GZKQ --- .../plans/2026-09-26-browser-ui-review.md | 15 +- .../browser/EmbeddedWebAppController.kt | 11 +- .../ui/screen/loggedIn/embed/ConsoleBridge.kt | 10 ++ .../favorites/EmbeddedNostrAppController.kt | 47 ++++- .../loggedIn/favorites/NostrAppScreen.kt | 89 +++++----- .../composeResources/values/strings.xml | 6 + .../browser/ui/pill/AccessInfoSheet.kt | 164 ++++++++++++++++++ .../browser/ui/pill/BrowserPillPreviews.kt | 17 ++ .../browser/ui/pill/BrowserPillRenderTest.kt | 2 + .../amethyst/napplethost/BrowserChromeHost.kt | 37 ++++ .../napplethost/NappletEmbedContract.kt | 28 +++ .../napplethost/NappletHostActivity.kt | 28 ++- .../napplethost/NappletHostService.kt | 68 ++++++++ nappletHost/src/main/res/values/strings.xml | 21 +-- 14 files changed, 447 insertions(+), 96 deletions(-) create mode 100644 commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/AccessInfoSheet.kt diff --git a/amethyst/plans/2026-09-26-browser-ui-review.md b/amethyst/plans/2026-09-26-browser-ui-review.md index f87c24a31b..2797131d3d 100644 --- a/amethyst/plans/2026-09-26-browser-ui-review.md +++ b/amethyst/plans/2026-09-26-browser-ui-review.md @@ -249,8 +249,13 @@ is gone. `EmbeddedPageDialogs`, `ConsoleLogEntry` and `BrowserWebTools.pageInfo`, plus the Android strings only they used. -Still open: -- The sandboxed apps' "What it can access" is still a platform `AlertDialog`. It lists launch - capabilities, not browser state. -- The embedded nsite/napplet tab has no find or text-size plumbing yet (`hasFind` / `hasTextSize` are - off there). +**Sandboxed apps, embedded and full screen alike:** +- An embedded nSite is labelled as an nSite, not "Sandboxed app", and gets the Tor row. Switching the + route saves it in `NappletNetworkRegistry` and rebuilds the session, as the full-screen host relaunches. +- "What it can access" is `AccessInfoSheet`: the launch capabilities, the keys-stay-in-Amethyst row, the + route (nSites), and Manage permissions. It replaces both the platform `AlertDialog` and the embedded + tab's `AccessDialog`. +- The embedded tab has find, text size and the console. `NappletEmbedContract` gained `MSG_FIND`, + `MSG_FIND_NEXT`, `MSG_FIND_RESULT`, `MSG_SET_TEXT_ZOOM` and `MSG_CONSOLE_LOG`, which + `NappletHostService` serves the same way `NappletBrowserService` does. Load and HTTP errors show up as + console errors, as they do full screen. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt index 4cd48b1181..8df5a2000f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt @@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.FindBridge import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.FindResult import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.ImeEvent import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.MagnifierFrame +import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.consoleLevelOf import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.parseImeEvent import java.util.concurrent.atomic.AtomicLong @@ -525,13 +526,3 @@ class EmbeddedWebAppController( private const val MAX_CONSOLE_LOGS = 200 } } - -/** Maps the provider's console level (WebView's `ConsoleMessage.MessageLevel` name) onto the chrome's. */ -private fun consoleLevelOf(level: String): ConsoleLine.Level = - when (level) { - "ERROR" -> ConsoleLine.Level.ERROR - "WARNING" -> ConsoleLine.Level.WARNING - "DEBUG" -> ConsoleLine.Level.DEBUG - "TIP" -> ConsoleLine.Level.INFO - else -> ConsoleLine.Level.LOG - } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/ConsoleBridge.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/ConsoleBridge.kt index 84e3bb70be..faa8cf7204 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/ConsoleBridge.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/ConsoleBridge.kt @@ -33,3 +33,13 @@ interface ConsoleBridge { fun clearConsoleLogs() } + +/** Maps a provider's console level (WebView's `ConsoleMessage.MessageLevel` name) onto the chrome's. */ +fun consoleLevelOf(level: String): ConsoleLine.Level = + when (level) { + "ERROR" -> ConsoleLine.Level.ERROR + "WARNING" -> ConsoleLine.Level.WARNING + "DEBUG" -> ConsoleLine.Level.DEBUG + "TIP" -> ConsoleLine.Level.INFO + else -> ConsoleLine.Level.LOG + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/EmbeddedNostrAppController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/EmbeddedNostrAppController.kt index 39d3fb0f6b..79a7ed1dd5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/EmbeddedNostrAppController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/EmbeddedNostrAppController.kt @@ -35,19 +35,27 @@ import android.os.Message import android.os.Messenger import android.os.SystemClock import androidx.annotation.RequiresApi +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory import androidx.privacysandbox.ui.client.view.SandboxedSdkView import androidx.privacysandbox.ui.core.SandboxedUiAdapter +import com.vitorpamplona.amethyst.commons.browser.ui.pill.ConsoleLine import com.vitorpamplona.amethyst.napplet.NappletWebViewProfiles import com.vitorpamplona.amethyst.napplet.WebFileChooserCoordinator import com.vitorpamplona.amethyst.napplethost.NappletEmbedContract import com.vitorpamplona.amethyst.napplethost.NappletHostContract +import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.ConsoleBridge import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedImeBridge import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedLoadStatus import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedMagnifierProbe import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedSurfaceController +import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.FindBridge +import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.FindResult import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.ImeEvent import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.MagnifierFrame +import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.consoleLevelOf import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.parseImeEvent import java.util.concurrent.atomic.AtomicLong @@ -67,7 +75,9 @@ class EmbeddedNostrAppController( private val params: Bundle, ) : EmbeddedSurfaceController, EmbeddedImeBridge, - EmbeddedMagnifierProbe { + EmbeddedMagnifierProbe, + ConsoleBridge, + FindBridge { private val incoming = Messenger(Handler(Looper.getMainLooper(), ::onServiceMessage)) private var serviceMessenger: Messenger? = null private var bound = false @@ -112,6 +122,14 @@ class EmbeddedNostrAppController( override var onMagnifierFrame: ((MagnifierFrame) -> Unit)? = null + /** The app's console output, capped at [MAX_CONSOLE_LOGS] entries. */ + override val consoleLogs = mutableStateListOf<ConsoleLine>() + + override fun clearConsoleLogs() = consoleLogs.clear() + + private val _findResult = mutableStateOf<FindResult?>(null) + override val findResult: State<FindResult?> = _findResult + private val connection = object : ServiceConnection { override fun onServiceConnected( @@ -266,6 +284,22 @@ class EmbeddedNostrAppController( } } } + NappletEmbedContract.MSG_FIND_RESULT -> { + val data = msg.data ?: return true + _findResult.value = FindResult(data.getInt(NappletEmbedContract.KEY_FIND_ACTIVE), data.getInt(NappletEmbedContract.KEY_FIND_TOTAL)) + } + NappletEmbedContract.MSG_CONSOLE_LOG -> { + val data = msg.data ?: return true + if (consoleLogs.size >= MAX_CONSOLE_LOGS) consoleLogs.removeAt(0) + consoleLogs.add( + ConsoleLine( + consoleLevelOf(data.getString(NappletEmbedContract.KEY_CONSOLE_LEVEL).orEmpty()), + data.getString(NappletEmbedContract.KEY_CONSOLE_MESSAGE).orEmpty(), + data.getString(NappletEmbedContract.KEY_CONSOLE_SOURCE).orEmpty(), + data.getInt(NappletEmbedContract.KEY_CONSOLE_LINE, 0), + ), + ) + } NappletEmbedContract.MSG_MAGNIFIER_FRAME -> { val data = msg.data ?: return true val bytes = data.getByteArray(NappletEmbedContract.KEY_MAG_BYTES) ?: return true @@ -305,6 +339,15 @@ class EmbeddedNostrAppController( fun reload() = send(NappletEmbedContract.MSG_RELOAD) + override fun find(query: String) { + if (query.isEmpty()) _findResult.value = null + send(NappletEmbedContract.MSG_FIND) { putString(NappletEmbedContract.KEY_FIND_QUERY, query) } + } + + override fun findNext(forward: Boolean) = send(NappletEmbedContract.MSG_FIND_NEXT) { putBoolean(NappletEmbedContract.KEY_FIND_FORWARD, forward) } + + fun setTextZoom(percent: Int) = send(NappletEmbedContract.MSG_SET_TEXT_ZOOM) { putInt(NappletEmbedContract.KEY_TEXT_ZOOM, percent) } + /** User-triggered recovery for a stuck or failed session: reload the verified content from scratch. */ override fun retry() { hasLoadedReal = false @@ -353,5 +396,7 @@ class EmbeddedNostrAppController( private companion object { private val SESSION_SEQ = AtomicLong() + + private const val MAX_CONSOLE_LOGS = 200 } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt index a3b12d3d61..3a7012cdcc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/favorites/NostrAppScreen.kt @@ -26,18 +26,19 @@ import androidx.activity.compose.BackHandler import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.AlertDialog +import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -50,11 +51,14 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.browser.ui.pill.AccessInfoSheet import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPillEvent import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPillUi import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp @@ -62,10 +66,6 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.navigation.favoriteIds import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.browser_unsupported -import com.vitorpamplona.amethyst.commons.resources.favorite_app_access_static -import com.vitorpamplona.amethyst.commons.resources.favorite_app_access_title -import com.vitorpamplona.amethyst.commons.resources.favorite_app_network_open -import com.vitorpamplona.amethyst.commons.resources.favorite_app_network_tor import com.vitorpamplona.amethyst.commons.resources.favorite_app_still_loading import com.vitorpamplona.amethyst.commons.resources.favorite_app_unavailable import com.vitorpamplona.amethyst.commons.resources.favorite_apps @@ -76,6 +76,7 @@ import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher import com.vitorpamplona.amethyst.favorites.FavoriteAppsRegistry +import com.vitorpamplona.amethyst.napplet.NappletNetworkRegistry import com.vitorpamplona.amethyst.napplethost.HostProfile import com.vitorpamplona.amethyst.napplethost.NappletEmbedContract import com.vitorpamplona.amethyst.napplethost.NappletHostContract @@ -133,7 +134,10 @@ private fun EmbeddedNostrAppTab( // Mint the verified launch params (a fresh token per resolve); null until the event loads. Re-minted // on a theme flip (the params carry the resolved theme into the sandbox host's WebView). - val params = remember(coordinate, EmbeddedTabHost.rebuildEpoch) { FavoriteAppLauncher.embedParams(context, coordinate) } + // Bumped when the user re-routes an nSite (Tor ↔ open web): the session is rebuilt with the new route, + // as the full-screen host relaunches itself. + var networkEpoch by remember(coordinate) { mutableIntStateOf(0) } + val params = remember(coordinate, EmbeddedTabHost.rebuildEpoch, networkEpoch) { FavoriteAppLauncher.embedParams(context, coordinate) } if (params == null) { UnavailableTab(coordinate, accountViewModel, nav) return @@ -145,16 +149,19 @@ private fun EmbeddedNostrAppTab( val capLabels = params.getStringArrayList(NappletHostContract.EXTRA_CAP_LABELS).orEmpty() val profile = HostProfile.fromName(params.getString(NappletHostContract.EXTRA_HOST_PROFILE)) val useTor = params.getBoolean(NappletHostContract.EXTRA_USE_TOR, true) + // Only nSites have a route of their own to choose, and only when Tor is running. + val torOn = if (profile.exposesNetwork && params.getInt(NappletHostContract.EXTRA_PROXY_PORT, -1) > 0) useTor else null val scope = rememberCoroutineScope() var canGoBack by remember { mutableStateOf(false) } var showAccess by remember { mutableStateOf(false) } + var textZoom by remember(coordinate) { mutableIntStateOf(BrowserChrome.DEFAULT_TEXT_ZOOM) } val apps by FavoriteAppsRegistry.favorites.collectAsStateWithLifecycle() val isFavorite = remember(apps, coordinate) { apps.any { it.id == "nostr:$coordinate" } } val controller = - remember(id, EmbeddedTabHost.rebuildEpoch) { + remember(id, EmbeddedTabHost.rebuildEpoch, networkEpoch) { EmbeddedTabFactory.acquireNostrApp(context, coordinate, params, backgroundColor) } @@ -174,30 +181,39 @@ private fun EmbeddedNostrAppTab( // Stable per app (title/coordinate/isFavorite don't change often), so the tab layer isn't recomposed every frame. val chrome = - remember(title, coordinate, isFavorite, controller) { + remember(title, coordinate, isFavorite, torOn, textZoom, controller) { EmbeddedTabChrome( ui = BrowserPillUi( title = title.ifBlank { coordinate }, chrome = BrowserChrome.State( - surface = BrowserChrome.Surface.NAPPLET, + surface = if (profile == HostProfile.WEBSITE) BrowserChrome.Surface.NSITE else BrowserChrome.Surface.NAPPLET, presentation = BrowserChrome.Presentation.EMBEDDED, url = "", startUrl = "", + torOn = torOn, hasAccessInfo = true, - // The embedded nsite/napplet host has no find or text-size plumbing (yet). - hasFind = false, - hasTextSize = false, ), isFavorite = isFavorite, + textZoom = textZoom, ), onEvent = { event -> + if (event is BrowserPillEvent.TextZoom) { + textZoom = event.percent + controller.setTextZoom(event.percent) + } when ((event as? BrowserPillEvent.Action)?.action) { BrowserChrome.Action.RELOAD -> controller.reload() BrowserChrome.Action.OPEN_FULL_SCREEN -> FavoriteAppLauncher.launch(context, FavoriteApp.NostrApp(coordinate, title, System.currentTimeMillis()), appStillLoadingStr) BrowserChrome.Action.ACCESS_INFO -> showAccess = true + BrowserChrome.Action.TOR -> { + // Persist the new route, then rebuild the session so it loads that way. + NappletNetworkRegistry.set(permissionCoordinate, !useTor) + EmbeddedTabHost.evict(id) + networkEpoch++ + } BrowserChrome.Action.SITE_SETTINGS -> nav.nav(Route.ConnectedAppDetail(permissionCoordinate)) BrowserChrome.Action.FAVORITE -> { val favId = "nostr:$coordinate" @@ -247,7 +263,22 @@ private fun EmbeddedNostrAppTab( BackHandler(enabled = canGoBack) { controller.back() } if (showAccess) { - AccessDialog(title, capLabels, profile.exposesNetwork, useTor) { showAccess = false } + Dialog(onDismissRequest = { showAccess = false }, properties = DialogProperties(usePlatformDefaultWidth = false)) { + Box(Modifier.fillMaxWidth().padding(16.dp), contentAlignment = Alignment.Center) { + AccessInfoSheet( + title = title.ifBlank { coordinate }, + isWebsite = profile == HostProfile.WEBSITE, + capabilities = capLabels, + torOn = torOn, + onManagePermissions = { + showAccess = false + nav.nav(Route.ConnectedAppDetail(permissionCoordinate)) + }, + onDone = { showAccess = false }, + modifier = Modifier.widthIn(max = 560.dp), + ) + } + } } Scaffold( @@ -295,36 +326,6 @@ private fun UnavailableTab( } } -@Composable -private fun AccessDialog( - title: String, - capLabels: List<String>, - showsNetwork: Boolean, - useTor: Boolean, - onDismiss: () -> Unit, -) { - val capsBody = - if (capLabels.isEmpty()) { - stringRes(Res.string.favorite_app_access_static) - } else { - capLabels.joinToString("\n") { "• $it" } - } - val networkBody = - if (showsNetwork) { - "\n\n" + stringRes(if (useTor) Res.string.favorite_app_network_tor else Res.string.favorite_app_network_open) - } else { - "" - } - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(if (title.isBlank()) stringRes(Res.string.favorite_app_access_title) else title) }, - text = { Text(capsBody + networkBody) }, - confirmButton = { - TextButton(onClick = onDismiss) { Text(stringRes(android.R.string.ok)) } - }, - ) -} - private fun noticeResId(notice: String): StringResource? = when (notice) { NappletEmbedContract.NOTICE_PUBLISHED -> Res.string.favorite_notice_published diff --git a/commonsUI/src/commonMain/composeResources/values/strings.xml b/commonsUI/src/commonMain/composeResources/values/strings.xml index d1e8fac7c4..49b19ed171 100644 --- a/commonsUI/src/commonMain/composeResources/values/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values/strings.xml @@ -5389,6 +5389,12 @@ <string name="browser_pill_site_settings_none">Nothing allowed yet</string> <string name="browser_pill_access">What it can access</string> <string name="browser_pill_access_desc">Your keys never leave Amethyst</string> + <string name="browser_pill_access_keys_desc">Every sign, publish, upload or payment goes through your approval</string> + <string name="browser_pill_access_none">No special access</string> + <string name="browser_pill_access_none_desc">It runs sandboxed and can't act on your account</string> + <string name="browser_pill_access_manage">Manage permissions</string> + <string name="browser_pill_access_nsite">nSite · sandboxed</string> + <string name="browser_pill_access_napplet">nApplet · sandboxed</string> <string name="browser_pill_console">Console</string> <string name="browser_pill_security_https">Secure connection</string> <string name="browser_pill_security_http">Not secure</string> diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/AccessInfoSheet.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/AccessInfoSheet.kt new file mode 100644 index 0000000000..e0bf7fdd64 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/AccessInfoSheet.kt @@ -0,0 +1,164 @@ +/* + * 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.commons.browser.ui.pill + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.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.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.browser_pill_access +import com.vitorpamplona.amethyst.commons.resources.browser_pill_access_desc +import com.vitorpamplona.amethyst.commons.resources.browser_pill_access_keys_desc +import com.vitorpamplona.amethyst.commons.resources.browser_pill_access_manage +import com.vitorpamplona.amethyst.commons.resources.browser_pill_access_napplet +import com.vitorpamplona.amethyst.commons.resources.browser_pill_access_none +import com.vitorpamplona.amethyst.commons.resources.browser_pill_access_none_desc +import com.vitorpamplona.amethyst.commons.resources.browser_pill_access_nsite +import com.vitorpamplona.amethyst.commons.resources.browser_pill_info_connection +import com.vitorpamplona.amethyst.commons.resources.browser_pill_info_open +import com.vitorpamplona.amethyst.commons.resources.browser_pill_info_tor +import com.vitorpamplona.amethyst.commons.resources.browser_pill_ok +import com.vitorpamplona.amethyst.commons.resources.browser_pill_tor_off +import com.vitorpamplona.amethyst.commons.resources.browser_pill_tor_on +import com.vitorpamplona.amethyst.commons.ui.stringRes + +/** + * "What it can access" for a sandboxed nSite or nApplet: the capabilities it was launched with, how it + * reaches the network (nSites only), and the promise that the keys stay in Amethyst. The sandboxed + * counterpart of [PageInfoSheet], drawn with the same header and grouped cards. + * + * [capabilities] are already-localized labels; empty means a static site with no special access. + * [torOn] is null when the app has no network route of its own to show. + */ +@Composable +fun AccessInfoSheet( + title: String, + isWebsite: Boolean, + capabilities: List<String>, + torOn: Boolean?, + onManagePermissions: (() -> Unit)?, + onDone: () -> Unit, + modifier: Modifier = Modifier, +) { + Surface( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surface, + shadowElevation = 6.dp, + border = PillDefaults.hairline(), + ) { + Column( + Modifier + .verticalScroll(rememberScrollState()) + .padding(PillDefaults.SheetPadding), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + SiteMonogram(title, size = 48.dp) + Spacer(Modifier.width(14.dp)) + Column { + Text(title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis) + Row(verticalAlignment = Alignment.CenterVertically) { + SecurityIcon(BrowserChrome.Security.SANDBOX, size = 14.dp) + Spacer(Modifier.width(4.dp)) + Text( + stringRes(if (isWebsite) Res.string.browser_pill_access_nsite else Res.string.browser_pill_access_napplet), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + + GroupCard(heading = stringRes(Res.string.browser_pill_access)) { + if (capabilities.isEmpty()) { + GroupRow( + icon = { Icon(MaterialSymbols.Shield, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant) }, + iconContainer = MaterialTheme.colorScheme.surfaceContainerHighest, + title = stringRes(Res.string.browser_pill_access_none), + supporting = stringRes(Res.string.browser_pill_access_none_desc), + onClick = null, + ) + } else { + capabilities.forEachIndexed { index, label -> + if (index > 0) GroupDivider() + GroupRow( + icon = { Icon(MaterialSymbols.CheckCircle, contentDescription = null, tint = MaterialTheme.colorScheme.onSecondaryContainer) }, + iconContainer = MaterialTheme.colorScheme.secondaryContainer, + title = label, + supporting = null, + onClick = null, + ) + } + } + GroupDivider() + GroupRow( + icon = { Icon(MaterialSymbols.Key, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant) }, + iconContainer = MaterialTheme.colorScheme.surfaceContainerHighest, + title = stringRes(Res.string.browser_pill_access_desc), + supporting = stringRes(Res.string.browser_pill_access_keys_desc), + onClick = null, + ) + } + + torOn?.let { tor -> + GroupCard(heading = stringRes(Res.string.browser_pill_info_connection)) { + GroupRow( + icon = { PillActionIcon(BrowserChrome.Action.TOR, tint = if (tor) MaterialTheme.colorScheme.onTertiaryContainer else MaterialTheme.colorScheme.onSurfaceVariant, size = 22.dp) }, + iconContainer = if (tor) MaterialTheme.colorScheme.tertiaryContainer else MaterialTheme.colorScheme.surfaceContainerHighest, + title = stringRes(if (tor) Res.string.browser_pill_info_tor else Res.string.browser_pill_info_open), + supporting = stringRes(if (tor) Res.string.browser_pill_tor_on else Res.string.browser_pill_tor_off), + onClick = null, + ) + } + } + + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically) { + if (onManagePermissions != null) { + TextButton(onClick = onManagePermissions) { Text(stringRes(Res.string.browser_pill_access_manage)) } + Spacer(Modifier.width(8.dp)) + } + Button(onClick = onDone) { Text(stringRes(Res.string.browser_pill_ok)) } + } + } + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillPreviews.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillPreviews.kt index ace3a1cf43..1a2531d2c4 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillPreviews.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillPreviews.kt @@ -238,3 +238,20 @@ fun PageInfoPreview() { } } } + +@Preview +@Composable +fun AccessInfoPreview() { + PreviewFrame { + Box(Modifier.padding(16.dp).width(380.dp)) { + AccessInfoSheet( + title = "Habla", + isWebsite = true, + capabilities = listOf("Sign events as you", "Publish to your relays", "Upload files"), + torOn = true, + onManagePermissions = {}, + onDone = {}, + ) + } + } +} diff --git a/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillRenderTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillRenderTest.kt index 3ac0d504e9..08711fed43 100644 --- a/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillRenderTest.kt +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPillRenderTest.kt @@ -89,6 +89,8 @@ class BrowserPillRenderTest { @Test fun pageInfo() = render("11-page-info", 820, 1250) { PageInfoPreview() } + @Test fun accessInfo() = render("12-access-info", 820, 610) { AccessInfoPreview() } + private companion object { const val SETTLE_FRAMES = 12 const val FRAME_MILLIS = 60L diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserChromeHost.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserChromeHost.kt index 5041339b99..a781eb4722 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserChromeHost.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/BrowserChromeHost.kt @@ -47,6 +47,7 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.commons.browser.BrowserChrome import com.vitorpamplona.amethyst.commons.browser.BrowserSitePermission +import com.vitorpamplona.amethyst.commons.browser.ui.pill.AccessInfoSheet import com.vitorpamplona.amethyst.commons.browser.ui.pill.AddressSuggestion import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserChromeTheme import com.vitorpamplona.amethyst.commons.browser.ui.pill.BrowserPill @@ -101,6 +102,15 @@ class BrowserChromeHost( fun onPanelsChanged() {} } + /** What a sandboxed app was launched with, for [showAccessInfo]. */ + class AccessInfo( + val title: String, + val isWebsite: Boolean, + val capabilities: List<String>, + val torOn: Boolean?, + val onManagePermissions: (() -> Unit)?, + ) + /** A page's JS dialog waiting for an answer. */ class PendingDialog( val type: PageDialogType, @@ -137,6 +147,7 @@ class BrowserChromeHost( var dialog by mutableStateOf<PendingDialog?>(null) var permissionPrompt by mutableStateOf<PendingPermission?>(null) private var pageInfoOpen by mutableStateOf(false) + private var accessInfo by mutableStateOf<AccessInfo?>(null) private var certificate by mutableStateOf<CertificateInfo?>(null) private var topView: ComposeView? = null @@ -203,6 +214,11 @@ class BrowserChromeHost( console.add(line) } + /** "What it can access" for a sandboxed nSite or nApplet. */ + fun showAccessInfo(info: AccessInfo) { + accessInfo = info + } + /** Page info for the page on screen, with its certificate when it has one. */ fun showPageInfo(certificate: CertificateInfo?) { this.certificate = certificate @@ -329,6 +345,27 @@ class BrowserChromeHost( ) } } + accessInfo?.let { info -> + Dialog(onDismissRequest = { accessInfo = null }, properties = DialogProperties(usePlatformDefaultWidth = false)) { + Box(Modifier.fillMaxWidth().padding(16.dp), contentAlignment = Alignment.Center) { + AccessInfoSheet( + title = info.title, + isWebsite = info.isWebsite, + capabilities = info.capabilities, + torOn = info.torOn, + onManagePermissions = + info.onManagePermissions?.let { manage -> + { + accessInfo = null + manage() + } + }, + onDone = { accessInfo = null }, + modifier = Modifier.widthIn(max = 560.dp), + ) + } + } + } if (pageInfoOpen) { Dialog(onDismissRequest = { pageInfoOpen = false }, properties = DialogProperties(usePlatformDefaultWidth = false)) { Box(Modifier.fillMaxWidth().padding(16.dp), contentAlignment = Alignment.Center) { diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletEmbedContract.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletEmbedContract.kt index 1f977a02d3..38df1171f7 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletEmbedContract.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletEmbedContract.kt @@ -115,6 +115,34 @@ object NappletEmbedContract { */ const val MSG_FILE_CHOOSER_RESULT = 19 + /** Client → provider: find [KEY_FIND_QUERY] in the page; an empty query clears the highlights. */ + const val MSG_FIND = 20 + + /** Client → provider: move to the next ([KEY_FIND_FORWARD] true) or previous match. */ + const val MSG_FIND_NEXT = 21 + + /** Provider → client: [KEY_FIND_ACTIVE] (0-based) of [KEY_FIND_TOTAL] matches. */ + const val MSG_FIND_RESULT = 22 + + /** Client → provider: set the page's text size to [KEY_TEXT_ZOOM] percent. */ + const val MSG_SET_TEXT_ZOOM = 23 + + /** + * Provider → client: one line for the developer console — [KEY_CONSOLE_LEVEL] (WebView's + * `ConsoleMessage.MessageLevel` name), [KEY_CONSOLE_MESSAGE], [KEY_CONSOLE_SOURCE], [KEY_CONSOLE_LINE]. + */ + const val MSG_CONSOLE_LOG = 24 + + const val KEY_FIND_QUERY = "findQuery" + const val KEY_FIND_FORWARD = "findForward" + const val KEY_FIND_ACTIVE = "findActive" + const val KEY_FIND_TOTAL = "findTotal" + const val KEY_TEXT_ZOOM = "textZoom" + const val KEY_CONSOLE_LEVEL = "consoleLevel" + const val KEY_CONSOLE_MESSAGE = "consoleMessage" + const val KEY_CONSOLE_SOURCE = "consoleSource" + const val KEY_CONSOLE_LINE = "consoleLine" + const val KEY_FILE_CHOOSER_ID = "fileChooserId" const val KEY_FILE_CHOOSER_ACCEPT = "fileChooserAccept" const val KEY_FILE_CHOOSER_MULTIPLE = "fileChooserMultiple" diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt index b16ab84463..e14cc25611 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.napplethost import android.annotation.SuppressLint -import android.app.AlertDialog import android.content.ComponentName import android.content.Intent import android.content.ServiceConnection @@ -213,9 +212,9 @@ class NappletHostActivity : ComponentActivity() { private val backCallback = object : OnBackPressedCallback(false) { override fun handleOnBackPressed() { - if (chrome?.handleBack() == true) { - Unit - } else if (this@NappletHostActivity::webView.isInitialized && !webViewGone && webView.canGoBack()) { + // The chrome first (open pill, find), then the applet's own history. + if (chrome?.handleBack() == true) return + if (this@NappletHostActivity::webView.isInitialized && !webViewGone && webView.canGoBack()) { webView.goBack() } else { isEnabled = false @@ -1047,18 +1046,15 @@ class NappletHostActivity : ComponentActivity() { /** Lists, in plain language, exactly which capabilities this napplet was launched with. */ private fun showAccessDialog() { - val body = - if (capabilityLabels.isEmpty()) { - getString(R.string.napplet_chrome_static_site) - } else { - capabilityLabels.joinToString("\n") { "• $it" } + "\n\n" + getString(R.string.napplet_chrome_keys_safe) - } - AlertDialog - .Builder(this) - .setTitle(getString(R.string.napplet_chrome_access_title, barTitle())) - .setMessage(body) - .setPositiveButton(android.R.string.ok, null) - .show() + chrome?.showAccessInfo( + BrowserChromeHost.AccessInfo( + title = barTitle(), + isWebsite = profile == HostProfile.WEBSITE, + capabilities = capabilityLabels, + torOn = if (profile.exposesNetwork && proxyPort > 0) useTor else null, + onManagePermissions = if (brokerMessenger != null) ::openPermissions else null, + ), + ) } /** diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostService.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostService.kt index b3a69b8d13..998117aad8 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostService.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostService.kt @@ -38,6 +38,7 @@ import android.os.Messenger import android.os.SystemClock import android.view.View import android.view.ViewGroup +import android.webkit.ConsoleMessage import android.webkit.JsPromptResult import android.webkit.JsResult import android.webkit.RenderProcessGoneDetail @@ -60,6 +61,7 @@ import androidx.webkit.ProxyController import androidx.webkit.WebMessageCompat import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature +import com.vitorpamplona.amethyst.commons.browser.BrowserChrome import com.vitorpamplona.amethyst.commons.napplet.NappletWebContract import com.vitorpamplona.amethyst.commons.util.booleanOrNull import com.vitorpamplona.amethyst.commons.util.parseJsonObjectOrNull @@ -129,6 +131,9 @@ class NappletHostService : Service() { // Last main-frame error state, pushed to the client so it can show an error/retry overlay over the // surface (the embedded surface has no error page of its own). var loadFailed = false + + // The user's text size, re-applied when a renderer crash forces a fresh WebView. + var textZoom = BrowserChrome.DEFAULT_TEXT_ZOOM val replyMessenger = Messenger(Handler(Looper.getMainLooper()) { onBrokerReply(this, it) }) } @@ -213,6 +218,17 @@ class NappletHostService : Service() { tab.bridgeReplyProxy?.postMessage(payload) } NappletEmbedContract.MSG_MAGNIFIER_REQUEST -> onMagnifierRequest(msg) + NappletEmbedContract.MSG_FIND -> { + val wv = tabFor(msg)?.webView ?: return true + val query = msg.data?.getString(NappletEmbedContract.KEY_FIND_QUERY).orEmpty() + if (query.isEmpty()) wv.clearMatches() else wv.findAllAsync(query) + } + NappletEmbedContract.MSG_FIND_NEXT -> tabFor(msg)?.webView?.findNext(msg.data?.getBoolean(NappletEmbedContract.KEY_FIND_FORWARD, true) ?: true) + NappletEmbedContract.MSG_SET_TEXT_ZOOM -> { + val tab = tabFor(msg) ?: return true + tab.textZoom = msg.data?.getInt(NappletEmbedContract.KEY_TEXT_ZOOM, tab.textZoom) ?: tab.textZoom + tab.webView?.let { BrowserWebTools.setTextZoom(it, tab.textZoom) } + } NappletEmbedContract.MSG_FILE_CHOOSER_RESULT -> { val tab = tabFor(msg) ?: return true val data = msg.data ?: return true @@ -362,6 +378,8 @@ class NappletHostService : Service() { wv.dropSystemBarInsets() if (tab.profile.exposesNetwork) applyWebViewProxy(effectiveProxy) WebViewCompat.addWebMessageListener(wv, NappletWebContract.BRIDGE_NAME, setOf(NappletWebContract.ORIGIN), ::onShellMessage) + wv.setFindListener { active, total, _ -> pushFindResult(tab, active, total) } + if (tab.textZoom != BrowserChrome.DEFAULT_TEXT_ZOOM) BrowserWebTools.setTextZoom(wv, tab.textZoom) tab.webView = wv wv.loadUrl(NappletWebContract.SHELL_URL) return wv @@ -426,6 +444,11 @@ class NappletHostService : Service() { fileChooserParams: FileChooserParams, ): Boolean = requestFileChooser(tab, filePathCallback, fileChooserParams) + override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { + pushConsoleLog(tab, consoleMessage.messageLevel().name, consoleMessage.message(), consoleMessage.sourceId(), consoleMessage.lineNumber()) + return true + } + // Setting a chrome client at all is what opts this WebView into the default JS-dialog handling, // and this one is built from a Service context — there is no window token to attach a dialog to, // and an applet's alert() must not be able to draw over the main app's trusted chrome anyway. @@ -547,6 +570,7 @@ class NappletHostService : Service() { request: WebResourceRequest, error: WebResourceError, ) { + pushConsoleLog(tab, ConsoleMessage.MessageLevel.ERROR.name, getString(R.string.napplet_console_load_error, error.errorCode, error.description?.toString().orEmpty()), request.url?.toString().orEmpty(), 0) // Only a main-frame failure blanks the applet; a missing sub-resource is irrelevant to whether // it opened. if (!request.isForMainFrame) return @@ -554,6 +578,14 @@ class NappletHostService : Service() { pushLoadState(tab, isLoading = false) } + override fun onReceivedHttpError( + view: WebView, + request: WebResourceRequest, + errorResponse: WebResourceResponse, + ) { + pushConsoleLog(tab, ConsoleMessage.MessageLevel.ERROR.name, getString(R.string.napplet_console_http_error, errorResponse.statusCode, errorResponse.reasonPhrase.orEmpty()), request.url?.toString().orEmpty(), 0) + } + /** * The renderer died. It is shared by every WebView in `:napplet`, and an unhandled crash kills the * whole process — every other tab included. Drop just this tab's WebView and report the load as @@ -599,6 +631,42 @@ class NappletHostService : Service() { runCatching { tab.clientMessenger?.send(message) } } + private fun pushFindResult( + tab: NappletTab, + active: Int, + total: Int, + ) { + val message = + Message.obtain(null, NappletEmbedContract.MSG_FIND_RESULT).apply { + data = + Bundle().apply { + putInt(NappletEmbedContract.KEY_FIND_ACTIVE, active) + putInt(NappletEmbedContract.KEY_FIND_TOTAL, total) + } + } + runCatching { tab.clientMessenger?.send(message) } + } + + private fun pushConsoleLog( + tab: NappletTab, + level: String, + text: String, + source: String, + line: Int, + ) { + val message = + Message.obtain(null, NappletEmbedContract.MSG_CONSOLE_LOG).apply { + data = + Bundle().apply { + putString(NappletEmbedContract.KEY_CONSOLE_LEVEL, level) + putString(NappletEmbedContract.KEY_CONSOLE_MESSAGE, text) + putString(NappletEmbedContract.KEY_CONSOLE_SOURCE, source) + putInt(NappletEmbedContract.KEY_CONSOLE_LINE, line) + } + } + runCatching { tab.clientMessenger?.send(message) } + } + /** Tells the client whether a main-frame load is in flight and whether it failed, so it can overlay a spinner/retry. */ private fun pushLoadState( tab: NappletTab, diff --git a/nappletHost/src/main/res/values/strings.xml b/nappletHost/src/main/res/values/strings.xml index 2ac71b58ff..d55e67ec81 100644 --- a/nappletHost/src/main/res/values/strings.xml +++ b/nappletHost/src/main/res/values/strings.xml @@ -4,30 +4,11 @@ <string name="napplet_invalid">Invalid nApplet.</string> <string name="napplet_webview_too_old">This device\'s WebView is too old to run nApplets safely.</string> - - <!-- Trusted chrome (top bar + live action notices) --> - <string name="napplet_chrome_access_title">What “%1$s” can access</string> - <string name="napplet_chrome_keys_safe">It can never read your keys, and every sign, publish, upload, or payment was approved by you. Manage access in Settings ▸ nApplets.</string> - <string name="napplet_chrome_static_site">Static site — it has no special access to your account.</string> - <string name="napplet_chrome_permissions_desc">What this app can access</string> - <string name="napplet_chrome_manage_permissions">Manage permissions</string> - <string name="napplet_chrome_reload">Reload</string> - <!-- Browser address bar and developer console strings are in :commons --> + <!-- Live "allow always" action notices --> <string name="napplet_action_published">“%1$s” published a note as you</string> <string name="napplet_action_uploaded">“%1$s” uploaded a file</string> <string name="napplet_action_paid">“%1$s” made a payment</string> - <!-- nSite network routing (Tor vs open web) --> - <string name="napplet_net_tor_desc">This site loads over Tor. Tap to change.</string> - <string name="napplet_net_open_desc">This site loads over the open web. Tap to change.</string> - <!-- Short labels for the pull-down sheet's network row --> - <string name="napplet_net_tor_label">Loads over Tor</string> - <string name="napplet_net_open_label">Loads over the open web</string> - - <!-- Favorite toggle in the pull-down sheet --> - <string name="browser_favorite_add">Add to favorites</string> - <string name="browser_favorite_remove">Remove from favorites</string> - <!-- Loading / unavailable screens --> <string name="napplet_unavailable_title">Couldn\'t load “%1$s”</string> <string name="napplet_unavailable_subtitle">The publisher\'s servers may be offline, or you\'re not connected. You can try again.</string> From 78e7561fb950766bddd968205940cbbb628d00f5 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 23:10:42 +0000 Subject: [PATCH 26/60] refactor(commonsUI): move the chat bubble group, and fix the round's audit findings The chat bubble layout, its group-position shapes, jumbo emoji sizing, the date/subject divisor and the scroll-to-newest effect move to commonsUI commons/chats/ui. Desktop keeps its older ChatBubbleLayout fork for now; the merge is the Desktop phase. Audit follow-ups on the previous commit: - PdfFetcher reads the disk cache from a provider on the IO dispatcher, so a cold-start PDF card no longer builds the app's lazy cache on main. - The Buzz DM lists resolve TimeAgoLabels once per list, not per row. - The iOS DateSkeletonFormatter rebuilds on a locale/time-zone change. - ScreenLayoutTest pins the inclusive 600dp breakpoint. - Stale KDoc links, a same-package import, and the baseline profile's stable-name entries for NowProvider and the moved theme functions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjQQN9CgWHVtNCSWqnKoqY --- .../baselineProfiles/baseline-prof.txt | 33 ++++++++--------- .../ui/components/pdf/PdfPreviewCard.kt | 2 +- .../ui/components/pdf/PdfViewerDialog.kt | 2 +- .../AccountSwitcherAndLeftDrawerLayout.kt | 2 +- .../screen/loggedIn/buzz/BuzzDmListScreen.kt | 10 ++++-- .../chats/cordnGroup/CordnGroupChatScreen.kt | 2 +- .../chats/cordnGroup/CordnMessageRow.kt | 10 +++--- .../loggedIn/chats/feed/ChatFeedView.kt | 4 ++- .../loggedIn/chats/feed/ChatMessageCompose.kt | 5 +-- .../chats/feed/types/RenderRegularTextNote.kt | 4 +-- .../relayGroup/RelayGroupChannelListScreen.kt | 8 ++++- .../cordn/CordnMessageGroupingTest.kt | 2 +- .../amethyst/ui/layouts/ScreenLayoutTest.kt | 4 +++ .../2026-08-30-commons-migration-sweep.md | 23 ++++++++++++ .../commons/chats/ui}/AutoScrollToNewest.kt | 4 +-- .../commons/chats/ui}/ChatBubbleLayout.kt | 3 +- .../commons/chats/ui}/ChatGroupPosition.kt | 4 +-- .../amethyst/commons/chats/ui}/JumboEmoji.kt | 2 +- .../chats/ui}/NewDateOrSubjectDivisor.kt | 3 +- .../ui/components/FileAttachmentCard.kt | 2 +- .../commons/ui/note/elements/TimeAgo.kt | 1 - .../commons/ui/note/PlatformDateFormat.ios.kt | 35 ++++++++++++++----- .../commons/service/pdf/PdfFetcher.kt | 8 +++-- 23 files changed, 116 insertions(+), 57 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui}/AutoScrollToNewest.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui}/ChatBubbleLayout.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui}/ChatGroupPosition.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui}/JumboEmoji.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui}/NewDateOrSubjectDivisor.kt (94%) diff --git a/amethyst/src/main/generated/baselineProfiles/baseline-prof.txt b/amethyst/src/main/generated/baselineProfiles/baseline-prof.txt index d760efd883..016fe51292 100644 --- a/amethyst/src/main/generated/baselineProfiles/baseline-prof.txt +++ b/amethyst/src/main/generated/baselineProfiles/baseline-prof.txt @@ -24768,17 +24768,17 @@ SPLcom/vitorpamplona/amethyst/ui/components/WindowUtilsKt;->getActivityOrNull(La Lcom/vitorpamplona/amethyst/commons/ui/navigation/topbars/ShorterTopAppBarKt; SPLcom/vitorpamplona/amethyst/commons/ui/navigation/topbars/ShorterTopAppBarKt;-><clinit>()V SPLcom/vitorpamplona/amethyst/commons/ui/navigation/topbars/ShorterTopAppBarKt;->getTopBarSize()F -Lcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt; -SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt;-><clinit>()V -SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt;->NowProvider(Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V -Lcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$$ExternalSyntheticLambda1; -SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$$ExternalSyntheticLambda1;-><init>()V -Lcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$NowProvider$now$1$1; -SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$NowProvider$now$1$1;-><init>(Lkotlin/coroutines/Continuation;)V -SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->invoke(Landroidx/compose/runtime/ProduceStateScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -SPLcom/vitorpamplona/amethyst/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; +Lcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt; +SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt;-><clinit>()V +SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt;->NowProvider(Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V +Lcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$$ExternalSyntheticLambda1; +SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$$ExternalSyntheticLambda1;-><init>()V +Lcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$NowProvider$now$1$1; +SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$NowProvider$now$1$1;-><init>(Lkotlin/coroutines/Continuation;)V +SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; +SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->invoke(Landroidx/compose/runtime/ProduceStateScope;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +SPLcom/vitorpamplona/amethyst/commons/ui/note/elements/NowProviderKt$NowProvider$now$1$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; Lcom/vitorpamplona/amethyst/ui/screen/AccountScreenKt; SPLcom/vitorpamplona/amethyst/ui/screen/AccountScreenKt;->AccountScreen$lambda$0(Landroidx/compose/runtime/State;)Lcom/vitorpamplona/amethyst/ui/screen/AccountState; SPLcom/vitorpamplona/amethyst/ui/screen/AccountScreenKt;->AccountScreen$lambda$2(Lcom/vitorpamplona/amethyst/ui/screen/AccountSessionManager;Lcom/vitorpamplona/amethyst/ui/screen/AccountState;Landroidx/compose/runtime/Composer;I)Lkotlin/Unit; @@ -25632,6 +25632,7 @@ SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSize40dp()F SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSize50dp()F SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSmallBorder()Landroidx/compose/foundation/shape/RoundedCornerShape; SPLcom/vitorpamplona/amethyst/commons/ui/theme/ShapeKt;->getSmallestBorder()Landroidx/compose/foundation/shape/RoundedCornerShape; +Lcom/vitorpamplona/amethyst/commons/ui/theme/AmethystThemeKt; Lcom/vitorpamplona/amethyst/ui/theme/ThemeKt; Lcom/vitorpamplona/amethyst/commons/ui/theme/MarkdownStyleKt; Lcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt; @@ -25648,13 +25649,13 @@ SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme$lambda$9(Landroid SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme(Lcom/vitorpamplona/amethyst/commons/model/ThemeType;Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;Lcom/vitorpamplona/amethyst/commons/model/FontFamilyType;Lcom/vitorpamplona/amethyst/commons/model/FontSizeType;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->AmethystTheme(Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->accentContainer-DxMtmZc(JZ)J -SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->accentPrimary(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;Z)J -SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->accentSecondary(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;Z)J -SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->darkColors(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;)Landroidx/compose/material3/ColorScheme; +SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystThemeKt;->accentPrimary(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;Z)J +SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystThemeKt;->accentSecondary(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;Z)J +SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystThemeKt;->amethystDarkColors(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;)Landroidx/compose/material3/ColorScheme; SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->getPlaceholderText(Landroidx/compose/material3/ColorScheme;)J SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->getTransparentBackground(Landroidx/compose/material3/ColorScheme;)J SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->isLight(Landroidx/compose/material3/ColorScheme;)Z -SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->lightColors(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;)Landroidx/compose/material3/ColorScheme; +SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystThemeKt;->amethystLightColors(Lcom/vitorpamplona/amethyst/commons/model/AccentColorType;)Landroidx/compose/material3/ColorScheme; SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->onAccent-8_81llA(J)J SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystColorSchemeKt;->onAccentContainer-DxMtmZc(JZ)J Lcom/vitorpamplona/amethyst/ui/theme/ThemeKt$$ExternalSyntheticLambda0; @@ -25685,7 +25686,7 @@ SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getDefaultParagraphSpaci SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getFont14SP()J SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getFont17SP()J SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->getTypography()Landroidx/compose/material3/Typography; -SPLcom/vitorpamplona/amethyst/ui/theme/ThemeKt;->toFontFamily(Lcom/vitorpamplona/amethyst/commons/model/FontFamilyType;)Landroidx/compose/ui/text/font/FontFamily; +SPLcom/vitorpamplona/amethyst/commons/ui/theme/AmethystThemeKt;->toFontFamily(Lcom/vitorpamplona/amethyst/commons/model/FontFamilyType;)Landroidx/compose/ui/text/font/FontFamily; SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt;->withFontFamily(Landroidx/compose/material3/Typography;Landroidx/compose/ui/text/font/FontFamily;)Landroidx/compose/material3/Typography; Lcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt$$ExternalSyntheticLambda0; SPLcom/vitorpamplona/amethyst/commons/ui/theme/TypeKt$$ExternalSyntheticLambda0;-><init>()V diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt index 4b72f34f30..7499c717c1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt @@ -158,7 +158,7 @@ private fun LoadedPdfPreviewCard( value = try { PdfFetcher - .fetchSnapshot(content.url, Amethyst.instance.diskCache) { url -> + .fetchSnapshot(content.url, { Amethyst.instance.diskCache }) { url -> accountViewModel.httpClientBuilder.okHttpClientForPreview(url) }.use { snapshot -> withContext(Dispatchers.IO) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt index e063369bc7..20e6408a72 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt @@ -196,7 +196,7 @@ private fun PdfViewerContent( try { withContext(Dispatchers.IO) { val snapshot = - PdfFetcher.fetchSnapshot(content.url, Amethyst.instance.diskCache) { url -> + PdfFetcher.fetchSnapshot(content.url, { Amethyst.instance.diskCache }) { url -> accountViewModel.httpClientBuilder.okHttpClientForPreview(url) } try { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt index 19d5be3a79..475ef9d787 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt @@ -273,7 +273,7 @@ private fun PermanentDrawerShell( /** * Hosts the navigation content. Screen width capping happens per NavHost destination - * ([com.vitorpamplona.amethyst.ui.layouts.CappedScreenContent] via the NavigationEffects + * ([com.vitorpamplona.amethyst.commons.ui.layouts.CappedScreenContent] via the NavigationEffects * builders), so this pane just claims the leftover row width. */ @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt index 341b508ca2..bac94781ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzDmListScreen.kt @@ -85,6 +85,7 @@ import com.vitorpamplona.amethyst.commons.resources.cancel import com.vitorpamplona.amethyst.commons.resources.remove_from_messages import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.TimeAgoLabels import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeAgoLabels import com.vitorpamplona.amethyst.commons.ui.note.timeAgoShort import com.vitorpamplona.amethyst.commons.ui.pluralStringRes @@ -127,6 +128,8 @@ fun BuzzDmListScreen( // Hidden conversations stay collapsed behind a header — they are off Messages by the user's own // choice, so they must not compete with the live inbox; they only need to be *reachable* again. var showHidden by remember { mutableStateOf(false) } + // Resolved once for the list, not per row. + val timeLabels = rememberTimeAgoLabels() Scaffold( topBar = { TopBarWithBackButton(stringRes(Res.string.buzz_dm_title), nav) }, @@ -149,7 +152,7 @@ fun BuzzDmListScreen( verticalArrangement = Arrangement.spacedBy(10.dp), ) { items(rows, key = { it.channelId }) { row -> - DmRowCard(row, isHidden = false, viewModel = viewModel, accountViewModel = accountViewModel, nav = nav) + DmRowCard(row, isHidden = false, viewModel = viewModel, timeLabels = timeLabels, accountViewModel = accountViewModel, nav = nav) } if (hiddenRows.isNotEmpty()) { item(key = "hidden-header") { @@ -161,7 +164,7 @@ fun BuzzDmListScreen( } if (showHidden) { items(hiddenRows, key = { "hidden-${it.channelId}" }) { row -> - DmRowCard(row, isHidden = true, viewModel = viewModel, accountViewModel = accountViewModel, nav = nav) + DmRowCard(row, isHidden = true, viewModel = viewModel, timeLabels = timeLabels, accountViewModel = accountViewModel, nav = nav) } } } @@ -211,6 +214,7 @@ private fun DmRowCard( row: BuzzDmListViewModel.DmRow, isHidden: Boolean, viewModel: BuzzDmListViewModel, + timeLabels: TimeAgoLabels, accountViewModel: AccountViewModel, nav: INav, ) { @@ -256,7 +260,7 @@ private fun DmRowCard( } Spacer(Modifier.width(8.dp)) Text( - text = timeAgoShort(row.lastActivity, rememberTimeAgoLabels()), + text = timeAgoShort(row.lastActivity, timeLabels), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt index b884f56dbd..f51173a565 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnGroupChatScreen.kt @@ -69,6 +69,7 @@ import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.chats.ui.AutoScrollToNewest import com.vitorpamplona.amethyst.commons.cordn.CordnGroupManager import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -121,7 +122,6 @@ import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.types.RenderAudioWaveformPlayer import com.vitorpamplona.amethyst.ui.pluralStringRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.AutoScrollToNewest import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.observeUserNameByHex import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt index 6d98298451..7c5696ca0d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnMessageRow.kt @@ -55,8 +55,13 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.chats.ui.CHAT_GROUP_WINDOW_SECONDS +import com.vitorpamplona.amethyst.commons.chats.ui.ChatBubbleLayout import com.vitorpamplona.amethyst.commons.chats.ui.ChatDivisor +import com.vitorpamplona.amethyst.commons.chats.ui.ChatGroupPosition import com.vitorpamplona.amethyst.commons.chats.ui.UserDisplayNameLayout +import com.vitorpamplona.amethyst.commons.chats.ui.jumboEmojiCount +import com.vitorpamplona.amethyst.commons.chats.ui.jumboEmojiFontSize import com.vitorpamplona.amethyst.commons.cordn.CordnMentions import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -109,11 +114,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ReactionChipView import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.SectionDivider import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.TileRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.authorNameColorFor -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.jumboEmojiCount -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.jumboEmojiFontSize -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.CHAT_GROUP_WINDOW_SECONDS -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatBubbleLayout -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatGroupPosition import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.observeUserNameByHex import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.coordinatorDisplayName import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt index d58a1422a7..25519ffbb2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt @@ -36,6 +36,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.chats.ui.AutoScrollToNewest +import com.vitorpamplona.amethyst.commons.chats.ui.NewDateOrSubjectDivisor +import com.vitorpamplona.amethyst.commons.chats.ui.watchChatGroupPosition import com.vitorpamplona.amethyst.commons.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.model.Note @@ -48,7 +51,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.watchChatGroupPosition import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt index ccb19c4c2d..27c29984a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt @@ -45,6 +45,9 @@ import androidx.compose.ui.Alignment.Companion.CenterStart import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.chats.ui.ChatBubbleLayout +import com.vitorpamplona.amethyst.commons.chats.ui.ChatGroupPosition +import com.vitorpamplona.amethyst.commons.chats.ui.jumboEmojiCount import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.latestBuzzEdit import com.vitorpamplona.amethyst.commons.model.latestConcordEdit @@ -60,8 +63,6 @@ import com.vitorpamplona.amethyst.ui.note.WatchBlockAndReport import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.DisplayZapSplits import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatBubbleLayout -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatGroupPosition import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzActivityRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzDiff import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderBuzzEditedNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt index eec5965811..ba2c0c0dfb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt @@ -26,6 +26,8 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import com.vitorpamplona.amethyst.commons.chats.ui.jumboEmojiCount +import com.vitorpamplona.amethyst.commons.chats.ui.jumboEmojiFontSize import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists @@ -40,8 +42,6 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderAudioFromIMeta import com.vitorpamplona.amethyst.ui.note.types.appendMissingImetaUrls import com.vitorpamplona.amethyst.ui.note.types.getAudioMetaWithWaveform import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.jumboEmojiCount -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.jumboEmojiFontSize @Composable fun RenderRegularTextNote( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt index 0b62d8571f..30efeb7f3b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelListScreen.kt @@ -99,6 +99,7 @@ import com.vitorpamplona.amethyst.commons.resources.relay_tor_clearnet_title import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.commons.ui.note.TimeAgoLabels import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeAgoLabels import com.vitorpamplona.amethyst.commons.ui.note.timeAgoShort import com.vitorpamplona.amethyst.commons.ui.pluralStringRes @@ -263,6 +264,8 @@ fun RelayGroupChannelListScreen( // as the workspace overflow menu (Add people / Invite), whose own doc notes "any member sees them, // the relay only serves the owner/admin ones." val myPubkey = accountViewModel.account.signer.pubKey + // Resolved once for the list, not per DM row. + val timeLabels = rememberTimeAgoLabels() val buzzVm: BuzzRelayImportViewModel = viewModel(key = "BuzzImport-${relay.url}") LaunchedEffect(relay, isBuzz) { if (isBuzz) buzzVm.bind(accountViewModel.account, relay.url) } @@ -644,6 +647,7 @@ fun RelayGroupChannelListScreen( BuzzDmInlineRow( row = row, myPubkey = myPubkey, + timeLabels = timeLabels, isHidden = false, accountViewModel = accountViewModel, nav = nav, @@ -675,6 +679,7 @@ fun RelayGroupChannelListScreen( BuzzDmInlineRow( row = row, myPubkey = myPubkey, + timeLabels = timeLabels, isHidden = true, accountViewModel = accountViewModel, nav = nav, @@ -828,6 +833,7 @@ private fun SectionAddButton( private fun BuzzDmInlineRow( row: BuzzDmListViewModel.DmRow, myPubkey: HexKey, + timeLabels: TimeAgoLabels, isHidden: Boolean, accountViewModel: AccountViewModel, nav: INav, @@ -885,7 +891,7 @@ private fun BuzzDmInlineRow( } if (row.lastActivity > 0) { Text( - text = timeAgoShort(row.lastActivity, rememberTimeAgoLabels()), + text = timeAgoShort(row.lastActivity, timeLabels), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/cordn/CordnMessageGroupingTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/cordn/CordnMessageGroupingTest.kt index 8a6066509a..8eebd64a2e 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/cordn/CordnMessageGroupingTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/cordn/CordnMessageGroupingTest.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.cordn +import com.vitorpamplona.amethyst.commons.chats.ui.ChatGroupPosition import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.cordnGroupPositionFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.sameDayAs -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatGroupPosition import com.vitorpamplona.quartz.cordn.spec02Envelopes.CordnDeliveredMessage import com.vitorpamplona.quartz.cordn.spec02Envelopes.CordnEnvelope import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt index d6600bd914..5c99632843 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt @@ -88,6 +88,10 @@ class ScreenLayoutTest { @Test fun compactWidthKeepsTheBottomBar() = assertStyle(NavigationStyle.BOTTOM_BAR, 599, 900) + // The Medium breakpoint is inclusive: exactly 600dp is no longer Compact. + @Test + fun exactlyAtTheMediumBreakpointRails() = assertStyle(NavigationStyle.NAV_RAIL, 600, 900) + // ---- Notification panel ---- @Test diff --git a/commons/plans/2026-08-30-commons-migration-sweep.md b/commons/plans/2026-08-30-commons-migration-sweep.md index 35693ab7e1..d24a6a3134 100644 --- a/commons/plans/2026-08-30-commons-migration-sweep.md +++ b/commons/plans/2026-08-30-commons-migration-sweep.md @@ -1501,3 +1501,26 @@ A transitive-blocker sweep of the 1,385 files under `amethyst/…/ui/`, after th - **Two corrections to the MOVE-AFTER table above:** - `INav`/`Route` are no longer blockers: every ui file imports the commons ones. - `ui/note/types` is 111 files, not ~89, and "AVM threading" understates it: the hubs matter more than the parameter. + +### Follow-up in the same round: the chat bubble group, and the audit + +- **Moved to `commonsUI/…/commons/chats/ui`**, beside `ChatDivisor` and + `UserDisplayNameLayout`: `ChatBubbleLayout`, `ChatGroupPosition`, `JumboEmoji`, + `NewDateOrSubjectDivisor` and `AutoScrollToNewest`. `AutoScrollToNewest` and + `CHAT_GROUP_WINDOW_SECONDS` went from `internal` to public so the app can reach them. + Desktop's `ui/chats/ChatBubbleLayout.kt` is an older fork of this one (no group + position, jumbo emoji, swipe-to-reply or reaction row). Replacing it is the Desktop + phase. +- **Audit follow-ups:** + - `PdfFetcher` takes the disk cache as a provider, read on the IO dispatcher, so a PDF + card composing on a cold start doesn't build the app's lazy cache on the main thread. + - The two DM lists resolve `TimeAgoLabels` once per list instead of once per row. + - The iOS `DateSkeletonFormatter` rebuilds on a locale or time-zone change, like the + other actuals. + - `ScreenLayoutTest` pins the inclusive 600dp boundary. + - Two stale KDoc links and a same-package import are fixed. + - The generated baseline profile has its stable-name entries repointed: + `NowProviderKt`, and the five theme functions now in `AmethystThemeKt`. Its R8 + lambda entries were already stale before this round (it still lists + `MarkDownStyleOnDark` under `ThemeKt`), so **regenerate the profile** after these + moves. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/AutoScrollToNewest.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/AutoScrollToNewest.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/AutoScrollToNewest.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/AutoScrollToNewest.kt index d2d06805f7..9480580f92 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/AutoScrollToNewest.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/AutoScrollToNewest.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed +package com.vitorpamplona.amethyst.commons.chats.ui import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable @@ -46,7 +46,7 @@ import androidx.compose.runtime.LaunchedEffect * message they just wrote somewhere off-screen below. */ @Composable -internal fun AutoScrollToNewest( +fun AutoScrollToNewest( listState: LazyListState, newest: Any?, mine: Boolean = false, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatBubbleLayout.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/ChatBubbleLayout.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatBubbleLayout.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/ChatBubbleLayout.kt index 67ae2a6d64..3583623b52 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatBubbleLayout.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/ChatBubbleLayout.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts +package com.vitorpamplona.amethyst.commons.chats.ui import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animate @@ -73,7 +73,6 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.chats.ui.UserDisplayNameLayout import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.ui.note.CommentIcon diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatGroupPosition.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/ChatGroupPosition.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatGroupPosition.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/ChatGroupPosition.kt index b710a72044..1ba16678a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/layouts/ChatGroupPosition.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/ChatGroupPosition.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts +package com.vitorpamplona.amethyst.commons.chats.ui import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable @@ -88,7 +88,7 @@ fun chatBubbleShapeFor( } /** Messages more than this far apart never group, even from the same author. */ -internal const val CHAT_GROUP_WINDOW_SECONDS = 10 * 60L +const val CHAT_GROUP_WINDOW_SECONDS = 10 * 60L /** * Event kinds that don't render as regular bubbles (zaps, raids, clips) or that diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/JumboEmoji.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/JumboEmoji.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/JumboEmoji.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/JumboEmoji.kt index 84d66d72b3..6f00e37c0f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/JumboEmoji.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/JumboEmoji.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed +package com.vitorpamplona.amethyst.commons.chats.ui import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.sp diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/NewDateOrSubjectDivisor.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/NewDateOrSubjectDivisor.kt index 355576e500..779bb759ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/NewDateOrSubjectDivisor.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/NewDateOrSubjectDivisor.kt @@ -18,11 +18,10 @@ * 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.chats.feed +package com.vitorpamplona.amethyst.commons.chats.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.commons.chats.ui.ChatDivisor import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.never diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileAttachmentCard.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileAttachmentCard.kt index 5960bb8db3..d0d1b13679 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileAttachmentCard.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/FileAttachmentCard.kt @@ -91,7 +91,7 @@ fun FileAttachmentCard( /** * The icon + title + subtitle row shared by every card that stands in for a file it can't * render inline: this one and the PDF placeholder/skeleton in - * [com.vitorpamplona.amethyst.ui.components.pdf.PdfPreviewCard]. + * the Android app's `PdfPreviewCard`. */ @Composable fun FileAttachmentRow( diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/TimeAgo.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/TimeAgo.kt index 1463dd1d88..636008baf9 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/TimeAgo.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/elements/TimeAgo.kt @@ -36,7 +36,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.TextUnit import com.vitorpamplona.amethyst.commons.model.Note -import com.vitorpamplona.amethyst.commons.ui.note.elements.LocalNowSeconds import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeAgoLabels import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter import com.vitorpamplona.amethyst.commons.ui.note.timeAbsoluteWith diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt index 060ad318f9..e64f02c89d 100644 --- a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt @@ -30,22 +30,41 @@ import platform.Foundation.NSDateFormatter import platform.Foundation.NSDateFormatterNoStyle import platform.Foundation.NSDateFormatterShortStyle import platform.Foundation.NSLocale +import platform.Foundation.NSTimeZone import platform.Foundation.currentLocale import platform.Foundation.dateWithTimeIntervalSince1970 +import platform.Foundation.defaultTimeZone +import platform.Foundation.localeIdentifier +import kotlin.concurrent.Volatile private fun dateOf(epochMillis: Long) = NSDate.dateWithTimeIntervalSince1970(epochMillis / 1000.0) -/** NSDateFormatter is thread-safe for formatting on iOS 7+; the template picks the locale's order. */ +/** + * NSDateFormatter is thread-safe for formatting on iOS 7+; the template picks the locale's order. + * Rebuilt when the current locale or time zone changes, like the other actuals. + */ actual class DateSkeletonFormatter actual constructor( - skeleton: String, + private val skeleton: String, ) { - private val formatter = - NSDateFormatter().apply { - locale = NSLocale.currentLocale - setLocalizedDateFormatFromTemplate(skeleton) - } + private class Cached( + val key: String, + val formatter: NSDateFormatter, + ) - actual fun format(epochMillis: Long): String = formatter.stringFromDate(dateOf(epochMillis)) + @Volatile private var cached: Cached? = null + + actual fun format(epochMillis: Long): String { + val locale = NSLocale.currentLocale + val key = locale.localeIdentifier + "|" + NSTimeZone.defaultTimeZone.name + val formatter = + cached?.takeIf { it.key == key }?.formatter + ?: NSDateFormatter() + .apply { + this.locale = locale + setLocalizedDateFormatFromTemplate(skeleton) + }.also { cached = Cached(key, it) } + return formatter.stringFromDate(dateOf(epochMillis)) + } } actual fun calendarYearAndDay(epochMillis: Long): Int { diff --git a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt index 6b562a668c..5af7e879e5 100644 --- a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt @@ -34,15 +34,17 @@ object PdfFetcher { * responsible for closing the returned snapshot; while it's open the cache entry cannot be * evicted, so the underlying file stays valid for `PdfRenderer`. * - * Pass the app's Coil [diskCache] so PDFs share the same LRU eviction and disk budget as - * images. + * Pass the app's Coil disk cache so PDFs share the same LRU eviction and disk budget as + * images. It is a provider, read on the IO dispatcher: the app builds its cache lazily + * (statvfs, directory setup), and a card composing on a cold start must not do that on main. */ suspend fun fetchSnapshot( url: String, - diskCache: DiskCache, + diskCache: () -> DiskCache, okHttpClient: (String) -> OkHttpClient, ): DiskCache.Snapshot = withContext(Dispatchers.IO) { + val diskCache = diskCache() // Covers the cache-hit fast path too, not just the download below it. openSnapshot() // contends on the global DiskLruCache lock, which Coil's cleanup pass holds across a // burst of unlink syscalls (see DeferredDeleteFileSystem) — calling it from a caller From 417f4060a7cb9d32e64bfedb8e3dcf0f73e86a82 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 23:40:07 +0000 Subject: [PATCH 27/60] fix: second audit of the commonsUI move - PDF snapshot close off main, shared formatters, tests - PdfPreviewCard closed its cache snapshot on the main thread (the use{} ran after withContext(IO) returned), taking Coil's global DiskLruCache lock there; pre-existing. PdfFetcher.useSnapshot fetches, runs the block and closes inside one IO block, with no suspension point for cancellation to leak through. The PDF viewer's onDispose closes its handle on the app IO scope, under the render mutex. - JVM and iOS built a time-of-day formatter per feed item; they share one cached instance now (NSDateFormatter is costly to build). - timeAgoShort no longer allocates an unused fallback lambda per tick on Android: the expect returns null where the platform has no short span. - The iOS skeleton formatter keys on the locale only, like Android: iOS caches its system zone until reset, so keying on it cost lookups and never saw a change. KDocs now say which platforms follow what. - JumboEmojiTest covers emoji counting (ZWJ, skin tone, flag, keycap) and the bubble shapes; ScreenLayoutTest moves to commonsUI jvmTest beside its code. - The plan records the audit, and that the packaged Desktop runtime lacks jdk.localedata, which these formatters need before Desktop uses them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjQQN9CgWHVtNCSWqnKoqY --- .../ui/components/pdf/PdfPreviewCard.kt | 14 ++- .../ui/components/pdf/PdfViewerDialog.kt | 10 +- .../2026-08-30-commons-migration-sweep.md | 51 +++++++--- .../ui/note/PlatformDateFormat.android.kt | 5 +- .../commons/ui/note/PlatformDateFormat.kt | 16 ++-- .../commons/ui/note/TimeAgoFormatter.kt | 2 +- .../commons/ui/note/PlatformDateFormat.ios.kt | 36 ++++---- .../commons/service/pdf/PdfFetcher.kt | 84 +++++++++++------ .../commons/ui/note/PlatformDateFormat.jvm.kt | 35 ++++--- .../commons/chats/ui/JumboEmojiTest.kt | 92 +++++++++++++++++++ .../commons}/ui/layouts/ScreenLayoutTest.kt | 5 +- 11 files changed, 255 insertions(+), 95 deletions(-) create mode 100644 commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/JumboEmojiTest.kt rename {amethyst/src/test/java/com/vitorpamplona/amethyst => commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons}/ui/layouts/ScreenLayoutTest.kt (93%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt index 7499c717c1..6e218e60c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt @@ -61,8 +61,6 @@ import com.vitorpamplona.amethyst.ui.components.ShareMediaAction import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext // Hard ceiling on the inline thumbnail bitmap, in pixels. Prevents OOM on very tall/large pages. private const val THUMBNAIL_MAX_DIM_PX = 1600 @@ -158,12 +156,12 @@ private fun LoadedPdfPreviewCard( value = try { PdfFetcher - .fetchSnapshot(content.url, { Amethyst.instance.diskCache }) { url -> - accountViewModel.httpClientBuilder.okHttpClientForPreview(url) - }.use { snapshot -> - withContext(Dispatchers.IO) { - renderFirstPage(snapshot.data.toFile(), targetWidthPx) - } + .useSnapshot( + url = content.url, + diskCache = { Amethyst.instance.diskCache }, + okHttpClient = { url -> accountViewModel.httpClientBuilder.okHttpClientForPreview(url) }, + ) { snapshot -> + renderFirstPage(snapshot.data.toFile(), targetWidthPx) }.also { result -> // Same cache the image and video paths use, so a PDF that has been rendered // once lays out at its real shape on every later visit instead of growing diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt index 20e6408a72..4376d12716 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfViewerDialog.kt @@ -82,6 +82,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext @@ -223,7 +224,14 @@ private fun PdfViewerContent( val handleForDispose = handleState DisposableEffect(handleForDispose) { onDispose { - handleForDispose?.close() + // Off the main thread: closing the cache snapshot takes Coil's global DiskLruCache + // lock, which its cleanup pass holds across bursts of unlinks. Under the render mutex, + // so a page render still in flight finishes before the renderer is closed under it. + handleForDispose?.let { handle -> + Amethyst.instance.applicationIOScope.launch { + handle.mutex.withLock { handle.close() } + } + } } } diff --git a/commons/plans/2026-08-30-commons-migration-sweep.md b/commons/plans/2026-08-30-commons-migration-sweep.md index d24a6a3134..bf9c67a45d 100644 --- a/commons/plans/2026-08-30-commons-migration-sweep.md +++ b/commons/plans/2026-08-30-commons-migration-sweep.md @@ -1439,7 +1439,7 @@ functional bugs. The follow-ups: | What | To | The blocker, and how it went | |---|---|---| -| `ScreenLayout` tier logic, `CappedScreenContent`, the pane widths | `commonsUI/…/ui/layouts/ScreenLayout.kt` | `material3-window-size-class` (app-only): its width breakpoints (Medium ≥ 600dp, Expanded ≥ 840dp) are inlined, and `ScreenLayoutTest`'s 17 cases pass against them. `LocalConfiguration` stays in the app, which asks the shared `rememberScreenLayoutSpec(widthDp, heightDp)`, so Desktop can supply its own window size. | +| `ScreenLayout` tier logic, `CappedScreenContent`, the pane widths | `commonsUI/…/ui/layouts/ScreenLayout.kt` | `material3-window-size-class` (app-only): its width breakpoints (Medium ≥ 600dp, Expanded ≥ 840dp) are inlined, and `ScreenLayoutTest` (18 cases, now in commonsUI jvmTest) passes against them. `LocalConfiguration` stays in the app, which asks the shared `rememberScreenLayoutSpec(widthDp, heightDp)`, so Desktop can supply its own window size. | | `animatedViewerChromeInset`, `rememberViewerControlsVisibility` | `commonsUI/…/ui/components/ViewerChrome.kt` | They sat in a file full of Android window code. Only `ImmersiveSystemBarsEffect` (Window/insets controller) stays. | | `SlidingCarousel` | `commonsUI/…/ui/components` | needed the inset above | | `AudioWaveformReadOnly` | `commonsUI/…/ui/components` | two enums from the Android-only audiowaveform library, now local enums with the same values | @@ -1472,12 +1472,13 @@ Desktop still has its own `ToggleableTimeAgoText` and the older `commons/…/uti A transitive-blocker sweep of the 1,385 files under `amethyst/…/ui/`, after this round: -- **80 files have no blocker left.** Many are headless and belong in `commons`, not `commonsUI`: - - filter assemblers and `*LastRead`; - - `NewMessageTagger`, `SplitConversor`, `PubKeyFormatter`, `SettingsCatalog`; - - the Tor status/dialog VM; - - `ChatBubbleLayout` + `ChatGroupPosition` + `JumboEmoji` + `NewDateOrSubjectDivisor` + `AutoScrollToNewest`. - - The chat bubble set is the next obvious batch: Desktop's `ui/chats/ChatBubbleLayout.kt` is an older fork of it. +- **80 files have no blocker left.** + - Many are headless and belong in `commons`, not `commonsUI`: the filter assemblers and + `*LastRead`, `NewMessageTagger`, `SplitConversor`, `PubKeyFormatter`, `SettingsCatalog`, + and the Tor status/dialog VM. + - The Compose ones include the chat bubble set (`ChatBubbleLayout`, `ChatGroupPosition`, + `JumboEmoji`, `NewDateOrSubjectDivisor`, `AutoScrollToNewest`). It moved to `commonsUI` + in the follow-up below. Desktop's `ui/chats/ChatBubbleLayout.kt` is an older fork of it. - **`AccountViewModel` is the wall:** - 892 files touch it, and 310 touch nothing else app-side. Yet swapping it for an interface frees only 76 files by itself, because the rest call hub composables that are blocked themselves. - The ui files use 210 distinct members of it. The note renderers use 28. @@ -1515,12 +1516,40 @@ A transitive-blocker sweep of the 1,385 files under `amethyst/…/ui/`, after th - `PdfFetcher` takes the disk cache as a provider, read on the IO dispatcher, so a PDF card composing on a cold start doesn't build the app's lazy cache on the main thread. - The two DM lists resolve `TimeAgoLabels` once per list instead of once per row. - - The iOS `DateSkeletonFormatter` rebuilds on a locale or time-zone change, like the - other actuals. + - The iOS `DateSkeletonFormatter` rebuilds on a locale change, as Android's does. (The + JVM one also keys on the time zone; iOS caches its system zone until reset, so keying on + it there would cost lookups and still not notice.) - `ScreenLayoutTest` pins the inclusive 600dp boundary. - Two stale KDoc links and a same-package import are fixed. - The generated baseline profile has its stable-name entries repointed: `NowProviderKt`, and the five theme functions now in `AmethystThemeKt`. Its R8 lambda entries were already stale before this round (it still lists - `MarkDownStyleOnDark` under `ThemeKt`), so **regenerate the profile** after these - moves. + `MarkDownStyleOnDark` under `ThemeKt`), and the new theme root + (`AmethystThemeKt;->AmethystMaterialTheme`, `isDarkTheme`) has no entries at all, so + **regenerate the profile** after these moves. + +### Second audit, and one Desktop decision + +- **Fixed:** + - `PdfPreviewCard` closed its cache snapshot on the main thread, after `withContext(IO)` + returned. That was older than this branch. Closing takes the same global DiskLruCache + lock as opening, so `PdfFetcher.useSnapshot` now fetches, runs the block and closes, all + in one IO block, leaving no suspension point for cancellation to leak through. + - The PDF viewer's `onDispose` now closes its handle on the app IO scope, under the + render mutex. + - JVM and iOS built a time-of-day formatter per feed item; they now share one cached + instance. `NSDateFormatter` is costly to build. + - `timeAgoShort` no longer allocates an unused fallback lambda per tick on Android. + - The iOS skeleton formatter keys on the locale only, like Android. iOS caches its system + zone until reset, so keying on the zone cost lookups and still never saw a change. + - New tests: `JumboEmojiTest` (counts, ZWJ/skin tone/flag/keycap sequences, bubble + shapes). `ScreenLayoutTest` moved to commonsUI jvmTest, beside the code it tests. +- **Desktop needs `jdk.localedata` before it uses these formatters.** + - `desktopApp/build.gradle.kts` `nativeDistributions.modules(...)` doesn't include it, + so the packaged runtime carries only en/root CLDR data. + - `DateTimeFormatter.ofLocalizedPattern` then gives en-GB `Jan 5, 2024` and de-DE + `2024 Jan 5`, and the JDK tests (full runtime) can't see it. + - Desktop's existing `java.time` formatting (`DesktopScheduleAtPicker`) already has the + same gap. + - Adding the module costs about +28 MB to the unpacked runtime. That is a packaging + call, left for the Desktop phase. diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt index 1aa0e916ea..385e7335e5 100644 --- a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.android.kt @@ -60,12 +60,11 @@ actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String { return remember(context) { { epochMillis -> DateFormat.getTimeFormat(context).format(Date(epochMillis)) } } } -actual fun relativeTimeSpanShort( +actual fun relativeTimeSpanShortOrNull( epochMillis: Long, nowMillis: Long, nowLabel: String, - fallback: () -> String, -): String { +): String? { val humanReadable = DateUtils .getRelativeTimeSpanString( diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt index 36bfd747a6..94711c83d9 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.kt @@ -25,7 +25,8 @@ import androidx.compose.runtime.Composable /** * Formats instants with a Unicode LDML date skeleton ("yMMMd", "MMMd", "yMMM"), letting the * platform pick the default locale's field order ("MMM d, y" in en-US, "d MMM y" in en-GB). - * Safe to call from any thread. Rebuilds itself when the default locale changes. + * Safe to call from any thread. Rebuilds itself when the default locale changes (the JVM actual + * also on a time-zone change; Android and iOS keep the zone they were built with). */ expect class DateSkeletonFormatter( skeleton: String, @@ -37,20 +38,19 @@ expect class DateSkeletonFormatter( expect fun calendarYearAndDay(epochMillis: Long): Int /** - * A time-of-day formatter ("14:32" / "2:32 PM") that follows the platform's own 12/24-hour - * preference, which on Android is a system setting rather than a locale property. + * A time-of-day formatter ("14:32" / "2:32 PM"). Android and iOS follow the system 12/24-hour + * setting; the JVM follows the locale. Identity-stable, so it can key a `remember`, and cheap to + * call for every item in a feed: platforms whose formatter is costly share one cached instance. */ @Composable expect fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String /** * The platform's abbreviated relative span ("5 min. ago", "Yesterday"), [nowLabel] for an - * instant that rounds to now or lies ahead, and [fallback] where the platform has no such - * formatter. + * instant that rounds to now or lies ahead, or null where the platform has no such formatter. */ -expect fun relativeTimeSpanShort( +expect fun relativeTimeSpanShortOrNull( epochMillis: Long, nowMillis: Long, nowLabel: String, - fallback: () -> String, -): String +): String? diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/TimeAgoFormatter.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/TimeAgoFormatter.kt index e0a8137456..649b149804 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/TimeAgoFormatter.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/TimeAgoFormatter.kt @@ -375,5 +375,5 @@ fun timeAgoShort( labels: TimeAgoLabels, ): String { if (time == null) return " " - return relativeTimeSpanShort(time * 1000, TimeUtils.nowMillis(), labels.now) { timeAgoWith(time, labels, prefix = "") } + return relativeTimeSpanShortOrNull(time * 1000, TimeUtils.nowMillis(), labels.now) ?: timeAgoWith(time, labels, prefix = "") } diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt index e64f02c89d..3a03a0d9e7 100644 --- a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.ios.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.commons.ui.note import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import platform.Foundation.NSCalendar import platform.Foundation.NSCalendarUnitDay import platform.Foundation.NSCalendarUnitYear @@ -30,10 +29,8 @@ import platform.Foundation.NSDateFormatter import platform.Foundation.NSDateFormatterNoStyle import platform.Foundation.NSDateFormatterShortStyle import platform.Foundation.NSLocale -import platform.Foundation.NSTimeZone import platform.Foundation.currentLocale import platform.Foundation.dateWithTimeIntervalSince1970 -import platform.Foundation.defaultTimeZone import platform.Foundation.localeIdentifier import kotlin.concurrent.Volatile @@ -41,7 +38,9 @@ private fun dateOf(epochMillis: Long) = NSDate.dateWithTimeIntervalSince1970(epo /** * NSDateFormatter is thread-safe for formatting on iOS 7+; the template picks the locale's order. - * Rebuilt when the current locale or time zone changes, like the other actuals. + * Rebuilt when the current locale changes, as on Android. The time zone is the one in effect when + * it was built: iOS caches the system zone until `NSTimeZone.resetSystemTimeZone()`, so keying + * on it would cost two lookups per call and still not notice a change. */ actual class DateSkeletonFormatter actual constructor( private val skeleton: String, @@ -55,7 +54,7 @@ actual class DateSkeletonFormatter actual constructor( actual fun format(epochMillis: Long): String { val locale = NSLocale.currentLocale - val key = locale.localeIdentifier + "|" + NSTimeZone.defaultTimeZone.name + val key = locale.localeIdentifier val formatter = cached?.takeIf { it.key == key }?.formatter ?: NSDateFormatter() @@ -75,21 +74,22 @@ actual fun calendarYearAndDay(epochMillis: Long): Int { return year * 1000 + day } -@Composable -actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String = - remember { - val formatter = - NSDateFormatter().apply { - dateStyle = NSDateFormatterNoStyle - timeStyle = NSDateFormatterShortStyle - } - val format: (Long) -> String = { epochMillis -> formatter.stringFromDate(dateOf(epochMillis)) } - format +// NSDateFormatter is costly to build, so every feed item shares this one instead of each +// composing its own. It follows the system 12/24-hour setting (the short time style does). +private val timeOfDayFormatter by lazy { + NSDateFormatter().apply { + dateStyle = NSDateFormatterNoStyle + timeStyle = NSDateFormatterShortStyle } +} -actual fun relativeTimeSpanShort( +private val timeOfDay: (Long) -> String = { epochMillis -> timeOfDayFormatter.stringFromDate(dateOf(epochMillis)) } + +@Composable +actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String = timeOfDay + +actual fun relativeTimeSpanShortOrNull( epochMillis: Long, nowMillis: Long, nowLabel: String, - fallback: () -> String, -): String = fallback() +): String? = null diff --git a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt index 5af7e879e5..f211373bb8 100644 --- a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/service/pdf/PdfFetcher.kt @@ -42,39 +42,63 @@ object PdfFetcher { url: String, diskCache: () -> DiskCache, okHttpClient: (String) -> OkHttpClient, - ): DiskCache.Snapshot = - withContext(Dispatchers.IO) { - val diskCache = diskCache() - // Covers the cache-hit fast path too, not just the download below it. openSnapshot() - // contends on the global DiskLruCache lock, which Coil's cleanup pass holds across a - // burst of unlink syscalls (see DeferredDeleteFileSystem) — calling it from a caller - // that happens to be on the main thread stalls the frame for that whole burst, and the - // hit path is exactly the one a feed takes when a PDF card scrolls back into view. - diskCache.openSnapshot(url)?.let { return@withContext it } + ): DiskCache.Snapshot = withContext(Dispatchers.IO) { openOrDownload(url, diskCache, okHttpClient) } - val editor = diskCache.openEditor(url) ?: throw IOException("Unable to open cache editor for $url") - try { - val request = - Request - .Builder() - .url(url) - .get() - .build() + /** The fetch itself. Callers run it on the IO dispatcher. */ + private suspend fun openOrDownload( + url: String, + diskCache: () -> DiskCache, + okHttpClient: (String) -> OkHttpClient, + ): DiskCache.Snapshot { + val cache = diskCache() + // Covers the cache-hit fast path too, not just the download below it. openSnapshot() + // contends on the global DiskLruCache lock, which Coil's cleanup pass holds across a + // burst of unlink syscalls (see DeferredDeleteFileSystem) — calling it from a caller + // that happens to be on the main thread stalls the frame for that whole burst, and the + // hit path is exactly the one a feed takes when a PDF card scrolls back into view. + cache.openSnapshot(url)?.let { return it } - okHttpClient(url).newCall(request).executeAsync().use { response -> - if (!response.isSuccessful) { - throw IOException("PDF download failed: ${response.code}") - } - diskCache.fileSystem.write(editor.data) { - val bytes = writeAll(response.body.source()) - if (bytes == 0L) throw IOException("PDF download failed: empty response body") - } + val editor = cache.openEditor(url) ?: throw IOException("Unable to open cache editor for $url") + try { + val request = + Request + .Builder() + .url(url) + .get() + .build() + + okHttpClient(url).newCall(request).executeAsync().use { response -> + if (!response.isSuccessful) { + throw IOException("PDF download failed: ${response.code}") + } + cache.fileSystem.write(editor.data) { + val bytes = writeAll(response.body.source()) + if (bytes == 0L) throw IOException("PDF download failed: empty response body") } - - editor.commitAndOpenSnapshot() ?: throw IOException("Unable to commit cache editor for $url") - } catch (t: Throwable) { - runCatching { editor.abort() } - throw t } + + return editor.commitAndOpenSnapshot() ?: throw IOException("Unable to commit cache editor for $url") + } catch (t: Throwable) { + runCatching { editor.abort() } + throw t + } + } + + /** + * Fetches like [fetchSnapshot], runs [block] on the snapshot, and closes it, all on the IO + * dispatcher. Prefer this to `fetchSnapshot(...).use { }` from a main-thread caller: there the + * close would run on main, and closing takes the same global DiskLruCache lock as opening. + * The snapshot is created by the last non-suspending step of the fetch and handed straight + * to `use`, inside one `withContext`, so a cancellation cannot land between the two and leak + * it open (which returning through [fetchSnapshot]'s own `withContext` could). + */ + suspend fun <T> useSnapshot( + url: String, + diskCache: () -> DiskCache, + okHttpClient: (String) -> OkHttpClient, + block: (DiskCache.Snapshot) -> T, + ): T = + withContext(Dispatchers.IO) { + openOrDownload(url, diskCache, okHttpClient).use(block) } } diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.jvm.kt index 18b5273f35..3ad02fe674 100644 --- a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.jvm.kt +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/PlatformDateFormat.jvm.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.commons.ui.note import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -58,17 +57,31 @@ actual class DateSkeletonFormatter actual constructor( } } -@Composable -actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String = - remember { - val formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withZone(ZoneId.systemDefault()) - val format: (Long) -> String = { epochMillis -> formatter.format(Instant.ofEpochMilli(epochMillis)) } - format - } +private class CachedTimeOfDay( + val locale: Locale, + val zone: ZoneId, + val formatter: DateTimeFormatter, +) -actual fun relativeTimeSpanShort( +@Volatile private var cachedTimeOfDay: CachedTimeOfDay? = null + +/** One formatter for every item, rebuilt only when the locale or zone changes. */ +private val timeOfDay: (Long) -> String = { epochMillis -> + val locale = Locale.getDefault() + val zone = ZoneId.systemDefault() + val formatter = + cachedTimeOfDay?.takeIf { it.locale == locale && it.zone == zone }?.formatter + ?: DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale).withZone(zone).also { + cachedTimeOfDay = CachedTimeOfDay(locale, zone, it) + } + formatter.format(Instant.ofEpochMilli(epochMillis)) +} + +@Composable +actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String = timeOfDay + +actual fun relativeTimeSpanShortOrNull( epochMillis: Long, nowMillis: Long, nowLabel: String, - fallback: () -> String, -): String = fallback() +): String? = null diff --git a/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/JumboEmojiTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/JumboEmojiTest.kt new file mode 100644 index 0000000000..1872d94557 --- /dev/null +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/chats/ui/JumboEmojiTest.kt @@ -0,0 +1,92 @@ +/* + * 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.commons.chats.ui + +import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeMe +import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeMeTop +import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeThemBottom +import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeThemMiddle +import org.junit.Assert.assertEquals +import org.junit.Assert.assertSame +import org.junit.Test + +// Emoji are written as escapes so the file stays unambiguous on disk (see CLAUDE.md). +class JumboEmojiTest { + private val grin = "\uD83D\uDE00" // U+1F600 + private val thumbsUpMedium = "\uD83D\uDC4D\uD83C\uDFFD" // U+1F44D + skin tone U+1F3FD + private val family = "\uD83D\uDC69\u200D\uD83D\uDC67" // woman ZWJ girl: draws as one + private val flagUs = "\uD83C\uDDFA\uD83C\uDDF8" // two regional indicators + private val keycapOne = "1\uFE0F\u20E3" // '1' + VS16 + combining keycap + private val redHeart = "\u2764\uFE0F" // U+2764 + VS16 + + @Test + fun countsUpToThreeEmoji() { + assertEquals(1, jumboEmojiCount(grin)) + assertEquals(2, jumboEmojiCount(grin + redHeart)) + assertEquals(3, jumboEmojiCount(grin + grin + grin)) + } + + @Test + fun moreThanThreeIsNotJumbo() = assertEquals(0, jumboEmojiCount(grin + grin + grin + grin)) + + @Test + fun anyTextMakesItNotJumbo() { + assertEquals(0, jumboEmojiCount("hi $grin")) + assertEquals(0, jumboEmojiCount("${grin}a")) + } + + @Test + fun whitespaceIsIgnored() = assertEquals(2, jumboEmojiCount(" $grin \n $grin ")) + + @Test + fun emptyOrBlankIsZero() { + assertEquals(0, jumboEmojiCount("")) + assertEquals(0, jumboEmojiCount(" ")) + } + + @Test + fun modifiersAndZwjSequencesCountAsOne() { + assertEquals(1, jumboEmojiCount(thumbsUpMedium)) + assertEquals(1, jumboEmojiCount(family)) + assertEquals(1, jumboEmojiCount(redHeart)) + } + + @Test + fun flagsNeedBothRegionalIndicators() { + assertEquals(1, jumboEmojiCount(flagUs)) + assertEquals(0, jumboEmojiCount("\uD83C\uDDFA")) // half a flag + } + + @Test + fun keycapsNeedTheirMark() { + assertEquals(1, jumboEmojiCount(keycapOne)) + assertEquals(0, jumboEmojiCount("1")) // a bare digit is text + assertEquals(0, jumboEmojiCount("1 $grin")) + } + + @Test + fun bubbleShapeFollowsSideAndPosition() { + assertSame(ChatBubbleShapeMe, chatBubbleShapeFor(true, ChatGroupPosition.SINGLE)) + assertSame(ChatBubbleShapeMeTop, chatBubbleShapeFor(true, ChatGroupPosition.TOP)) + assertSame(ChatBubbleShapeThemMiddle, chatBubbleShapeFor(false, ChatGroupPosition.MIDDLE)) + assertSame(ChatBubbleShapeThemBottom, chatBubbleShapeFor(false, ChatGroupPosition.BOTTOM)) + } +} diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/ScreenLayoutTest.kt similarity index 93% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt rename to commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/ScreenLayoutTest.kt index 5c99632843..a52dd5f1eb 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/layouts/ScreenLayoutTest.kt +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/layouts/ScreenLayoutTest.kt @@ -18,11 +18,8 @@ * 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.layouts +package com.vitorpamplona.amethyst.commons.ui.layouts -import com.vitorpamplona.amethyst.commons.ui.layouts.NavigationStyle -import com.vitorpamplona.amethyst.commons.ui.layouts.decideNavigationStyle -import com.vitorpamplona.amethyst.commons.ui.layouts.hasRoomForNotificationPanel import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue From fa7e8dba7da5eb50775f9057dabba44be9f3bd5b Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sat, 26 Sep 2026 23:54:56 +0000 Subject: [PATCH 28/60] fix(quartz): hash empty input on Apple without crashing sha256() and sha256Into() in Sha256.apple.kt pinned the input and took addressOf(0), which throws ArrayIndexOutOfBoundsException on Kotlin/Native when the array is empty. So sha256(ByteArray(0)) crashed on iOS/macOS, and test-quartz-ios failed 7 OversizedTransferTest cases that hash "" (red on main too). CommonCrypto's CC_SHA256 accepts a null data pointer for a zero-length input, so pass null when the array is empty. Adds Sha256Test with FIPS 180-2 vectors (empty, "abc", prefix hashing through sha256Into) so every platform's actual is checked, not only through CEP-22. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015VvJ8XpeYqBe8bYT3bJ7UD --- .../quartz/utils/sha256/Sha256.apple.kt | 6 +- .../quartz/utils/sha256/Sha256Test.kt | 58 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Test.kt diff --git a/quartz/src/appleMain/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.apple.kt b/quartz/src/appleMain/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.apple.kt index e21f31f86d..6d9058379a 100644 --- a/quartz/src/appleMain/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.apple.kt +++ b/quartz/src/appleMain/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256.apple.kt @@ -34,7 +34,8 @@ actual fun sha256(data: ByteArray): ByteArray { data.usePinned { inputPinned -> digest.usePinned { digestPinned -> CC_SHA256( - inputPinned.addressOf(0), + // addressOf(0) throws on an empty array; CommonCrypto accepts null for a 0-byte input. + if (data.isEmpty()) null else inputPinned.addressOf(0), data.size.convert(), digestPinned.addressOf(0), ) @@ -53,7 +54,8 @@ actual fun sha256Into( data.usePinned { inputPinned -> out.asUByteArray().usePinned { digestPinned -> CC_SHA256( - inputPinned.addressOf(0), + // addressOf(0) throws on an empty array; CommonCrypto accepts null for a 0-byte input. + if (data.isEmpty()) null else inputPinned.addressOf(0), len.convert(), digestPinned.addressOf(0), ) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Test.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Test.kt new file mode 100644 index 0000000000..d58c57ddb0 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/sha256/Sha256Test.kt @@ -0,0 +1,58 @@ +/* + * 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.quartz.utils.sha256 + +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Known-answer vectors (FIPS 180-2) for every platform's sha256 actual. The empty + * input case guards the Apple actual, which used to pin `addressOf(0)` on a + * zero-length array and throw ArrayIndexOutOfBoundsException. + */ +class Sha256Test { + private val emptyDigest = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + private val abcDigest = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" + + private fun ByteArray.hex() = joinToString("") { (it.toInt() and 0xFF).toString(16).padStart(2, '0') } + + @Test + fun hashesEmptyInput() { + assertEquals(emptyDigest, sha256(ByteArray(0)).hex()) + } + + @Test + fun hashesAbc() { + assertEquals(abcDigest, sha256("abc".encodeToByteArray()).hex()) + } + + @Test + fun sha256IntoHashesEmptyInput() { + assertEquals(emptyDigest, sha256Into(ByteArray(32), ByteArray(0)).hex()) + } + + @Test + fun sha256IntoHashesOnlyThePrefix() { + val out = ByteArray(32) + assertEquals(abcDigest, sha256Into(out, "abcdef".encodeToByteArray(), 3).hex()) + assertEquals(emptyDigest, sha256Into(out, "abcdef".encodeToByteArray(), 0).hex()) + } +} From 0dca3cb11b125af60b777a36d2bab00b060b8a47 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona <vitor@vitorpamplona.com> Date: Sat, 26 Sep 2026 20:42:32 -0400 Subject: [PATCH 29/60] fix(browser): the two things that looked wrong in the pill and the Recent list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **The tile grid stranded its remainder.** Six actions filled a row of four and left two beside a hole. The rows now share the tiles out evenly — six is 3 + 3, five is 3 + 2 — and a gap only appears when the count will not divide, which for seven is 4 + 3 either way. Rows are still padded back out to four where they draw, so every tile keeps the same width. `balancedRowSizes` returns the row sizes rather than one column count: the first shape of this returned a single number and `chunked` it, which quietly gave 4 + 4 + 2 for ten instead of 4 + 3 + 3. The test caught that, so the function changed rather than the expectation. **Two rows in Recent both read "Primal / primal.net".** They are not duplicates — they are `primal.net/home` and `primal.net`, two pages that happen to share a `<title>`. The second line was only ever the host, so there was no way to tell them apart or to know which one a tap would open. It now shows host and path, minus the scheme and a bare trailing slash: `primal.net/home` against `primal.net`. While in there, `BrowserHistoryRegistry` now keys on a normalised URL instead of the raw string. That is not what caused the pair above, but a trailing slash, a different case in the host or a leftover `#fragment` would each have produced a genuine duplicate, and `distinctBy { it.url }` could not see it. Verified on an SM-T220: the Recent rows read `primal.net/home` and `primal.net`, and the six-tile pill renders as two rows of three. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- .../favorites/BrowserHistoryRegistry.kt | 36 ++++++++- .../screen/loggedIn/browser/BrowserScreen.kt | 23 +++++- .../favorites/BrowserHistoryKeyTest.kt | 69 ++++++++++++++++ .../amethyst/favorites/RecentSubtitleTest.kt | 62 ++++++++++++++ .../commons/browser/ui/pill/BrowserPill.kt | 39 +++++++-- .../browser/ui/pill/BalancedRowSizesTest.kt | 80 +++++++++++++++++++ 6 files changed, 299 insertions(+), 10 deletions(-) create mode 100644 amethyst/src/test/java/com/vitorpamplona/amethyst/favorites/BrowserHistoryKeyTest.kt create mode 100644 amethyst/src/test/java/com/vitorpamplona/amethyst/favorites/RecentSubtitleTest.kt create mode 100644 commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BalancedRowSizesTest.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/BrowserHistoryRegistry.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/BrowserHistoryRegistry.kt index 5564139886..57494e95f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/BrowserHistoryRegistry.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/favorites/BrowserHistoryRegistry.kt @@ -101,8 +101,9 @@ object BrowserHistoryRegistry { ) { val host = OmniboxInput.hostOf(url) ?: url val now = System.currentTimeMillis() + val key = historyKey(url) update { current -> - val existing = current.firstOrNull { it.url == url } + val existing = current.firstOrNull { historyKey(it.url) == key } val entry = if (existing != null) { existing.copy( @@ -114,18 +115,22 @@ object BrowserHistoryRegistry { } else { BrowserHistoryEntry(url = url, title = title, host = host, lastVisitedAt = now, visitCount = 1) } - (listOf(entry) + current.filterNot { it.url == url }).take(MAX_ENTRIES) + (listOf(entry) + current.filterNot { historyKey(it.url) == key }).take(MAX_ENTRIES) } } - fun remove(url: String) = update { current -> current.filterNot { it.url == url } } + fun remove(url: String) = + update { current -> + val key = historyKey(url) + current.filterNot { historyKey(it.url) == key } + } fun clear() = update { emptyList() } private fun dedupeNewestFirst(list: List<BrowserHistoryEntry>): List<BrowserHistoryEntry> = list .sortedByDescending { it.lastVisitedAt } - .distinctBy { it.url } + .distinctBy { historyKey(it.url) } .take(MAX_ENTRIES) private inline fun update(transform: (List<BrowserHistoryEntry>) -> List<BrowserHistoryEntry>) { @@ -152,3 +157,26 @@ object BrowserHistoryRegistry { emptyList() } } + +/** + * What counts as "the same page" in the history list. + * + * Keying on the raw URL string put `primal.net` in the list twice: a trailing slash, a different case + * in the host, or a leftover `#fragment` reads as one page and compares as two Strings. So the scheme + * and host are lowercased, a fragment is dropped, and a trailing slash is dropped when the path is + * nothing but that slash. + * + * The path and the query are kept and compared as-is. Two pages of the same site are two entries, and + * guessing which query parameters are load-bearing is how you lose one of them. + */ +internal fun historyKey(url: String): String { + val noFragment = url.substringBefore('#') + val schemeEnd = noFragment.indexOf("://") + if (schemeEnd <= 0) return noFragment + val scheme = noFragment.substring(0, schemeEnd).lowercase() + val rest = noFragment.substring(schemeEnd + 3) + val authorityEnd = rest.indexOfFirst { it == '/' || it == '?' } + val authority = (if (authorityEnd < 0) rest else rest.substring(0, authorityEnd)).lowercase() + val tail = if (authorityEnd < 0) "" else rest.substring(authorityEnd) + return scheme + "://" + authority + if (tail == "/") "" else tail +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserScreen.kt index 06f038fb22..f65ff428d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserScreen.kt @@ -745,7 +745,11 @@ private fun RecentRow( overflow = TextOverflow.Ellipsis, ) Text( - entry.host, + // Host and path, not just the host: two pages of one site usually carry the + // same <title>, so `primal.net/home` and `primal.net` arrived as two rows + // reading "Primal / primal.net" and there was no way to tell them apart or + // to know which one a tap would open. + remember(entry.url, entry.host) { recentSubtitle(entry.url, entry.host) }, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, @@ -818,3 +822,20 @@ private fun SiteIcon( /** The host of [url] for a favorite's default label, falling back to the raw string. */ private fun hostOf(url: String): String = OmniboxInput.hostOf(url) ?: url + +/** + * The second line of a Recent row: what a reader needs to tell two rows of one site apart. + * + * The scheme and a bare trailing slash are noise at this size, so they go; everything after the + * host stays, because that is the part that differs. Falls back to the host when the URL has + * nothing more to say. + */ +internal fun recentSubtitle( + url: String, + host: String, +): String { + val schemeEnd = url.indexOf("://") + val afterScheme = if (schemeEnd > 0) url.substring(schemeEnd + 3) else url + val trimmed = afterScheme.removeSuffix("/") + return trimmed.ifBlank { host } +} diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/favorites/BrowserHistoryKeyTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/favorites/BrowserHistoryKeyTest.kt new file mode 100644 index 0000000000..842fe99427 --- /dev/null +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/favorites/BrowserHistoryKeyTest.kt @@ -0,0 +1,69 @@ +/* + * 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.favorites + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test + +/** The list showed `primal.net` twice; these are the pairs that produced that. */ +class BrowserHistoryKeyTest { + @Test + fun `a trailing slash on the root is the same page`() { + assertEquals(historyKey("https://primal.net"), historyKey("https://primal.net/")) + } + + @Test + fun `the host is compared without case`() { + assertEquals(historyKey("https://Primal.NET/"), historyKey("https://primal.net/")) + } + + @Test + fun `a fragment is not a different page`() { + assertEquals(historyKey("https://primal.net/home"), historyKey("https://primal.net/home#top")) + } + + @Test + fun `a path is still its own page`() { + assertNotEquals(historyKey("https://primal.net/"), historyKey("https://primal.net/home")) + } + + @Test + fun `a query is still its own page`() { + assertNotEquals(historyKey("https://x.com/search?q=a"), historyKey("https://x.com/search?q=b")) + } + + @Test + fun `a trailing slash deeper in the path is left alone`() { + // Servers are free to treat these as different, so we do not decide for them. + assertNotEquals(historyKey("https://primal.net/home"), historyKey("https://primal.net/home/")) + } + + @Test + fun `http and https are different origins`() { + assertNotEquals(historyKey("http://primal.net/"), historyKey("https://primal.net/")) + } + + @Test + fun `something that is not a url is returned as itself`() { + assertEquals("not a url", historyKey("not a url")) + } +} diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/favorites/RecentSubtitleTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/favorites/RecentSubtitleTest.kt new file mode 100644 index 0000000000..988495bced --- /dev/null +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/favorites/RecentSubtitleTest.kt @@ -0,0 +1,62 @@ +/* + * 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.favorites + +import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.recentSubtitle +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test + +/** The two rows that both read "Primal / primal.net" and could not be told apart. */ +class RecentSubtitleTest { + @Test + fun `two pages of one site no longer read the same`() { + assertNotEquals( + recentSubtitle("https://primal.net/", "primal.net"), + recentSubtitle("https://primal.net/home", "primal.net"), + ) + } + + @Test + fun `the root shows the bare host`() { + assertEquals("primal.net", recentSubtitle("https://primal.net/", "primal.net")) + } + + @Test + fun `a path is shown after the host`() { + assertEquals("primal.net/home", recentSubtitle("https://primal.net/home", "primal.net")) + } + + @Test + fun `a port is kept, since it is part of where you are going`() { + assertEquals("localhost:8000/t.html", recentSubtitle("http://localhost:8000/t.html", "localhost")) + } + + @Test + fun `a query is kept`() { + assertEquals("x.com/search?q=nostr", recentSubtitle("https://x.com/search?q=nostr", "x.com")) + } + + @Test + fun `a url with nothing to add falls back to the host`() { + assertEquals("primal.net", recentSubtitle("", "primal.net")) + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPill.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPill.kt index ca09cc1bc8..b0b5d88e14 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPill.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BrowserPill.kt @@ -353,7 +353,7 @@ private fun OutOfScopeBanner( } } -/** Page actions as tiles, four per row; toggles (desktop site, text size) fill while on. */ +/** Page actions as tiles, at most four per row; toggles (desktop site, text size) fill while on. */ @Composable private fun TileGrid( actions: List<Action>, @@ -361,9 +361,16 @@ private fun TileGrid( textSizeOpen: Boolean, onAction: (Action) -> Unit, ) { - val columns = 4 + // Spread over the rows we are going to use anyway, rather than filling each + // one to four and leaving the remainder stranded. Six actions read as 3 + 3 + // instead of 4 + 2 with a hole beside it, and five as 3 + 2 instead of 4 + 1. + // Every row is chunked to the same width, so the tiles stay a uniform size + // and a gap appears only when the count is not divisible — seven is 4 + 3 + // either way. + var taken = 0 + val rows = balancedRowSizes(actions.size, MAX_TILE_COLUMNS).map { size -> actions.subList(taken, taken + size).also { taken += size } } Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - actions.chunked(columns).forEach { rowActions -> + rows.forEach { rowActions -> Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { rowActions.forEach { action -> // The hand-off tile names the browser it will actually open when the @@ -384,13 +391,35 @@ private fun TileGrid( modifier = Modifier.weight(1f), ) } - // Keep the last row's tiles the same width as the others. - repeat(columns - rowActions.size) { Spacer(Modifier.weight(1f)) } + // Keep every row's tiles the same width, whatever the row holds. + repeat(MAX_TILE_COLUMNS - rowActions.size) { Spacer(Modifier.weight(1f)) } } } } } +/** Most tiles on one row. Four is what fits at the pill's width without the labels wrapping twice. */ +private const val MAX_TILE_COLUMNS = 4 + +/** + * How many tiles go on each row, spread as evenly as the count allows. + * + * The number of rows is fixed by [max] either way — this only decides how the + * items are shared out between them, so the leftovers do not all land on the + * last row. Ten tiles are 4 + 3 + 3, not 4 + 4 + 2. Rows are padded back out + * to [max] where they draw, so the tiles stay a uniform width throughout. + */ +internal fun balancedRowSizes( + count: Int, + max: Int, +): List<Int> { + if (count <= 0) return emptyList() + val rows = (count + max - 1) / max + val base = count / rows + val remainder = count % rows + return List(rows) { index -> base + if (index < remainder) 1 else 0 } +} + /** Text size: a stepped slider between a small and a large "A", the value, and a way back to 100%. */ @Composable fun TextSizeControl( diff --git a/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BalancedRowSizesTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BalancedRowSizesTest.kt new file mode 100644 index 0000000000..787661cdf2 --- /dev/null +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/browser/ui/pill/BalancedRowSizesTest.kt @@ -0,0 +1,80 @@ +/* + * 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.commons.browser.ui.pill + +import kotlin.test.Test +import kotlin.test.assertEquals + +class BalancedRowSizesTest { + private fun rows( + count: Int, + max: Int = 4, + ) = balancedRowSizes(count, max) + + @Test + fun `six fills two rows evenly instead of stranding two`() { + assertEquals(listOf(3, 3), rows(6)) + } + + @Test + fun `five is three and two, not four and one`() { + assertEquals(listOf(3, 2), rows(5)) + } + + @Test + fun `seven cannot be even and stays four and three`() { + assertEquals(listOf(4, 3), rows(7)) + } + + @Test + fun `a full row is left alone`() { + assertEquals(listOf(4), rows(4)) + assertEquals(listOf(4, 4), rows(8)) + } + + @Test + fun `no row is ever wider than the maximum, and every tile is placed`() { + (1..24).forEach { count -> + val rows = rows(count) + assertEquals(true, rows.all { it in 1..4 }, "count=$count rows=$rows") + assertEquals(count, rows.sum(), "count=$count rows=$rows") + } + } + + @Test + fun `rows never differ by more than one`() { + (1..24).forEach { count -> + val rows = rows(count) + assertEquals(true, rows.max() - rows.min() <= 1, "count=$count rows=$rows") + } + } + + @Test + fun `it never puts everything on one row it cannot fit`() { + assertEquals(listOf(4, 4, 4), rows(12)) + assertEquals(listOf(4, 3, 3), rows(10)) + } + + @Test + fun `an empty grid does not divide by zero`() { + assertEquals(emptyList(), balancedRowSizes(0, 4)) + } +} From 400c15ff445fde6d0af04aa18eac50226caa2e12 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sun, 27 Sep 2026 01:03:18 +0000 Subject: [PATCH 30/60] feat(quartz): NIP-FE relay commands over HTTP, and the engine seams it needs A relay can now answer one client command per HTTP request (POST /req, /count, /event, /neg) with its own NIP-01 frames, one per line, ending on the command's answer, through the same session, policies and backend as its websocket. The spec draft is NIP-FE, "Relay Commands over HTTP". Engine (nip01Core/relay/server), source- and binary-compatible: - SessionSink: where a session's frames go, typed (Message) wherever the engine built one and raw for the spliced EVENT frames. A transport can now end an answer at its EOSE or map a CLOSED onto a status without re-parsing wire JSON. SessionSink.of(send) is the old string callback, and RelaySession/RelayServerBase keep every (String) -> Unit overload. - RelaySession.initialAuthenticatedUsers, RelayServerBase.connect(sink, authenticatedUsers) and serve(sink, authenticatedUsers, incoming): a transport that proved a key itself (NIP-98) opens the session signed in, recorded exactly where a NIP-42 AUTH would record it, so every policy and backend that reads RequestContext.authenticatedUsers sees it with no side table. - RelaySession.receive(Command): dispatch an already-parsed command. The string overload parses and calls it; acceptMessage still guards text. NIP-FE (nipFERelayOverHttp), common code, no HTTP library: - HttpRelayCommand: the four paths, each body parsed into its typed Command through a JSON tree (so a body can only ever be arguments), and which Message ends each answer. - HttpRelayStatus: the status an answer's first frame gives it, from its NIP-01 prefix; a duplicate: OK is a 200. - HttpRelayHandler: size, parse, NIP-98 (payload-bound, single-use, its own replay cache, any of the relay's addresses, other schemes ignored), then the exchange: first frame decides the status, the rest streams through HttpRelayResponse/HttpRelayLines, flushed whenever no frame is waiting. The deadline is read between frames and never interrupts a write; a reader that stops reading is HttpRelayReaderStalled past a grace, and the host drops it. Admission stays with the host, in front of handle(), so a refused request never spends a token. - NEG is one stateless NIP-77 round, [filter, message]: the responder's only state is its sealed snapshot, which the backend already caches per filter, so nothing is held between rounds and no NEG-CLOSE exists. Not in this change: backpressure from the socket into the backend. The store callbacks cannot suspend, so a reader more than maxQueuedFrames behind is cut with a CLOSED line, the websocket's bound. Verified with :quartz:jvmTest (HttpRelayHandlerTest, 13 cases over a fake in-memory SessionBackend), and earlier against the a8e8778265 jar that vespa-relay pins, where every call the rest of the jar makes into RelaySession/RelayServerBase still links. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jDNhr6a3J4VC5TaG3Yd48 --- .../nip01Core/relay/server/RelayServerBase.kt | 26 +- .../nip01Core/relay/server/RelaySession.kt | 42 +- .../nip01Core/relay/server/SessionSink.kt | 54 +++ .../nipFERelayOverHttp/HttpRelayCommand.kt | 139 +++++++ .../nipFERelayOverHttp/HttpRelayHandler.kt | 295 ++++++++++++++ .../nipFERelayOverHttp/HttpRelayStatus.kt | 65 ++++ .../HttpRelayHandlerTest.kt | 363 ++++++++++++++++++ 7 files changed, 974 insertions(+), 10 deletions(-) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/SessionSink.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayCommand.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayStatus.kt create mode 100644 quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/RelayServerBase.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/RelayServerBase.kt index 7423b0804b..f1449f0261 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/RelayServerBase.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/RelayServerBase.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.nip01Core.relay.server +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.server.backend.SessionBackend import com.vitorpamplona.quartz.nip01Core.relay.server.policies.IRelayPolicy import com.vitorpamplona.quartz.nip01Core.relay.server.policies.LimitsPolicy @@ -79,15 +80,27 @@ abstract class RelayServerBase( * @param send Callback the server uses to send JSON messages to this client. * Implementations must be safe to call from any coroutine. */ - fun connect(send: (String) -> Unit): RelaySession = + fun connect(send: (String) -> Unit): RelaySession = connect(SessionSink.of(send)) + + /** + * Registers a new client connection whose frames go to [sink], typed where + * the engine has the type, already signed in as [authenticatedUsers] when + * the transport proved them itself (a NIP-98 header; see + * [RelaySession.initialAuthenticatedUsers]). + */ + fun connect( + sink: SessionSink, + authenticatedUsers: Set<HexKey> = emptySet(), + ): RelaySession = connections.register( RelaySession( policy = buildPolicy(), store = backend, scope = scope, - onSend = send, + sink = sink, onClose = { connections.unregister(it.id) }, negentropySettings = negentropySettings, + initialAuthenticatedUsers = authenticatedUsers, ), ) @@ -102,8 +115,15 @@ abstract class RelayServerBase( suspend fun serve( send: (String) -> Unit, incoming: suspend (RelaySession) -> Unit, + ) = serve(SessionSink.of(send), emptySet(), incoming) + + /** [serve] over a [SessionSink], signed in as [authenticatedUsers] from the start. */ + suspend fun serve( + sink: SessionSink, + authenticatedUsers: Set<HexKey>, + incoming: suspend (RelaySession) -> Unit, ) { - val session = connect(send) + val session = connect(sink, authenticatedUsers) try { incoming(session) } finally { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/RelaySession.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/RelaySession.kt index 33a01e0e92..68793aa486 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/RelaySession.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/RelaySession.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NoticeMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.AuthCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CloseCmd +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CountCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd @@ -67,7 +68,8 @@ class RelaySession( private val store: SessionBackend, val policy: IRelayPolicy, private val scope: CoroutineScope, - private val onSend: (String) -> Unit, + /** Where every frame for this client goes; see [SessionSink] for the typed/raw split. */ + private val sink: SessionSink, private val onClose: (RelaySession) -> Unit, negentropySettings: NegentropySettings = NegentropySettings.Default, /** @@ -77,7 +79,26 @@ class RelaySession( * open/close of the same connection. Defaults to a fresh monotonic id. */ val id: Long = nextConnectionId(), + /** + * Identities the transport proved before the session existed — a + * NIP-98 header on an HTTP command (NIP-FE), say — recorded exactly as + * a NIP-42 AUTH would record them. The policy's `accept(AuthCmd)` and + * `onAuthenticated` are not consulted: there is no AUTH event, and the + * transport, not the engine, did the verifying. + */ + initialAuthenticatedUsers: Set<HexKey> = emptySet(), ) : AutoCloseable { + /** The original, string-only constructor; every frame goes to [onSend] as wire JSON. */ + constructor( + store: SessionBackend, + policy: IRelayPolicy, + scope: CoroutineScope, + onSend: (String) -> Unit, + onClose: (RelaySession) -> Unit, + negentropySettings: NegentropySettings = NegentropySettings.Default, + id: Long = nextConnectionId(), + ) : this(store, policy, scope, SessionSink.of(onSend), onClose, negentropySettings, id) + private val subscriptions = LargeCache<String, Job>() /** @@ -95,7 +116,7 @@ class RelaySession( * the copy costs nothing on the hot path. */ @Volatile - private var authenticatedUsers = setOf<HexKey>() + private var authenticatedUsers: Set<HexKey> = initialAuthenticatedUsers.toSet() /** * The per-connection scope. Handed to the [policy] at connect (so gating @@ -133,10 +154,7 @@ class RelaySession( fun send(message: Message) { try { - // message.toJson() defaults to OptimizedJsonMapper.toJson(this) for - // every type; NegMsgMessage overrides it with a direct-build wire - // path (identical output, ~2× faster on big reconcile frames). - onSend(message.toJson()) + sink.message(message) } catch (e: Exception) { Log.w("ClientSession") { "Failed to send to ${e.message}" } } @@ -145,7 +163,7 @@ class RelaySession( /** [send] for frames that are already wire-format JSON (the raw REQ path). */ private fun sendRaw(json: String) { try { - onSend(json) + sink.raw(json) } catch (e: Exception) { Log.w("ClientSession") { "Failed to send to ${e.message}" } } @@ -175,6 +193,16 @@ class RelaySession( return } + receive(cmd) + } + + /** + * Dispatches an already-parsed command, for a transport that parsed and + * sized it itself (NIP-FE's HTTP bodies). [IRelayPolicy.acceptMessage] + * is not run here — it judges wire text — so such a caller applies the + * message-length limit before calling. + */ + suspend fun receive(cmd: Command) { if (!cmd.isValid()) { send(NoticeMessage("error: invalid command")) return diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/SessionSink.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/SessionSink.kt new file mode 100644 index 0000000000..fa44c56925 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/SessionSink.kt @@ -0,0 +1,54 @@ +/* + * 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.quartz.nip01Core.relay.server + +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message + +/** + * Where a [RelaySession]'s frames go. The engine hands over a [Message] + * wherever it has one, so a transport that must react to a frame (end an + * HTTP answer at its EOSE, map a CLOSED onto a status) reads the type + * instead of re-parsing wire JSON; stored and live EVENT frames, which the + * zero-decode REQ path builds as text, arrive through [raw]. + * + * Both are called from the engine's coroutines, on any thread, and must not + * block. Deliberately not a `fun interface`: a lambda would be ambiguous with + * the `(String) -> Unit` overloads that predate it. + */ +interface SessionSink { + /** A frame the engine built as a [Message]. */ + fun message(message: Message) + + /** A frame already in wire JSON: an `EVENT` spliced from stored or live text. */ + fun raw(json: String) + + companion object { + /** Every frame to [send] as wire JSON, which is all a socket wants. */ + fun of(send: (String) -> Unit): SessionSink = + object : SessionSink { + // NegMsgMessage overrides toJson with a direct-build wire path + // (identical output, ~2× faster on big reconcile frames). + override fun message(message: Message) = send(message.toJson()) + + override fun raw(json: String) = send(json) + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayCommand.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayCommand.kt new file mode 100644 index 0000000000..304735bf06 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayCommand.kt @@ -0,0 +1,139 @@ +/* + * 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.quartz.nipFERelayOverHttp + +import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ClosedMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EoseMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NoticeMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CountCmd +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd +import com.vitorpamplona.quartz.nip77Negentropy.NegErrMessage +import com.vitorpamplona.quartz.nip77Negentropy.NegMsgMessage +import com.vitorpamplona.quartz.nip77Negentropy.NegOpenCmd +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive + +/** + * NIP-FE: the client commands HTTP carries, one path each. A body is the + * command's arguments after its subscription id (a lone object where the + * command takes one); the answer ends on the first frame [ends] accepts. + * + * `NEG` is one NIP-77 round, `[filter, message]`: the responder keeps no + * state between rounds but its snapshot, which the backend caches per + * filter, so each round carries its filter and there is no session to close. + */ +enum class HttpRelayCommand( + val path: String, +) { + REQ("/req"), + COUNT("/count"), + EVENT("/event"), + NEG("/neg"), + ; + + /** + * The command [body] stands for, parsed and validated, or null when it + * is not this command's arguments. Re-serialized from a JSON tree, so a + * body can only ever be arguments, never a second command. + */ + fun parse(body: String): Parsed? { + val tree = + try { + Json.parseToJsonElement(body) + } catch (_: SerializationException) { + return null + } + val args = arguments(tree) ?: return null + val frame = JsonArray(head() + args).toString() + val cmd = runCatching { OptimizedJsonMapper.fromJsonToCommand(frame) }.getOrNull() ?: return null + return if (cmd.isValid() && cmd.matches()) Parsed(cmd, frame.length) else null + } + + /** A body as its command, with the length of the frame it stands for: what the message-length limit measures. */ + class Parsed( + val command: Command, + val wireLength: Int, + ) + + /** Whether [message] is the last frame of this command's answer. */ + fun ends(message: Message): Boolean = + message is NoticeMessage || + when (this) { + REQ -> message is EoseMessage || message is ClosedMessage + COUNT -> message is CountMessage || message is ClosedMessage + EVENT -> message is OkMessage + NEG -> message is NegMsgMessage || message is NegErrMessage + } + + private fun head(): List<JsonElement> = + when (this) { + REQ -> listOf(JsonPrimitive(ReqCmd.LABEL), JsonPrimitive(SUB_ID)) + COUNT -> listOf(JsonPrimitive(CountCmd.LABEL), JsonPrimitive(SUB_ID)) + EVENT -> listOf(JsonPrimitive(EventCmd.LABEL)) + NEG -> listOf(JsonPrimitive(NegOpenCmd.LABEL), JsonPrimitive(SUB_ID)) + } + + private fun arguments(body: JsonElement): List<JsonElement>? = + when (this) { + REQ, COUNT -> { + when (body) { + is JsonObject -> listOf(body) + is JsonArray -> body.takeIf { it.isNotEmpty() && it.all { f -> f is JsonObject } } + else -> null + } + } + + EVENT -> { + (body as? JsonObject)?.let(::listOf) + } + + NEG -> { + (body as? JsonArray)?.takeIf { + it.size == 2 && it[0] is JsonObject && (it[1] as? JsonPrimitive)?.isString == true + } + } + } + + private fun Command.matches(): Boolean = + when (this@HttpRelayCommand) { + REQ -> this is ReqCmd + COUNT -> this is CountCmd + EVENT -> this is EventCmd + NEG -> this is NegOpenCmd + } + + companion object { + /** The subscription id every HTTP command runs under; each request is its own connection. */ + const val SUB_ID = "http" + + fun forPath(path: String): HttpRelayCommand? = entries.firstOrNull { it.path == path } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt new file mode 100644 index 0000000000..e28f86815d --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt @@ -0,0 +1,295 @@ +/* + * 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.quartz.nipFERelayOverHttp + +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.AuthMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ClosedMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.MachineReadablePrefix +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message +import com.vitorpamplona.quartz.nip01Core.relay.server.RelayServerBase +import com.vitorpamplona.quartz.nip01Core.relay.server.SessionSink +import com.vitorpamplona.quartz.nip98HttpAuth.Nip98AuthVerifier +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.withTimeoutOrNull +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.time.TimeSource + +/** One NIP-FE request as the handler needs it. The host routes by [HttpRelayCommand.path] and bounds the body while reading it. */ +class HttpRelayRequest( + val command: HttpRelayCommand, + /** The `Authorization` header as sent, or null. */ + val authorization: String?, + val body: ByteArray, +) + +/** + * Where [HttpRelayHandler] writes an answer. The host owns the socket, the + * headers and any compression; the handler decides the status and the lines. + * On 401 the host adds `WWW-Authenticate: Nostr`, on 429 and 503 `Retry-After`. + */ +interface HttpRelayResponse { + /** An answer that is one frame, with its status: a refusal, or a command answered at once. */ + suspend fun single( + status: Int, + frame: String, + ) + + /** + * A 200 answer written line by line, as `application/x-ndjson`. Returns + * when [lines] does; a [HttpRelayReaderStalled] thrown out of it means + * the client stopped reading and the host drops the connection unfinished. + */ + suspend fun stream(lines: suspend HttpRelayLines.() -> Unit) +} + +/** The body of a streamed answer. */ +interface HttpRelayLines { + /** One frame and its newline. May suspend on the socket: that is the backpressure. */ + suspend fun line(frame: String) + + /** Sends what [line] has written so far; called whenever no frame is waiting. */ + suspend fun flush() +} + +/** The client stopped reading a streamed answer; the host drops the connection instead of finishing it. */ +class HttpRelayReaderStalled : Exception("the client stopped reading the answer") + +/** + * NIP-FE: one relay command per HTTP request, run on its own [RelayServerBase] + * session, so every limit and policy the socket applies applies here, and + * answered with the relay's own frames up to the command's answer. Nothing + * outlives the request. Admission (how many requests a client may run) is the + * host's: gate before calling [handle], so a refused request does not spend a + * NIP-98 token that the handler would have verified. + */ +class HttpRelayHandler( + private val server: RelayServerBase, + /** The prefixes a NIP-98 `u` may carry (the relay's http origin, its .onion), asked per request; never from the request. */ + private val origins: () -> List<String>, + /** How long one answer may run, first byte to last. */ + private val deadlineMs: Long = DEFAULT_DEADLINE_MS, + /** Its own replay cache, so public commands cannot evict another endpoint's. */ + private val verifier: Nip98AuthVerifier = Nip98AuthVerifier(), + /** Frames queued ahead of a slow reader before the answer is cut short. */ + private val maxQueuedFrames: Int = DEFAULT_MAX_QUEUED_FRAMES, + /** How long past the deadline the last line may take before the reader counts as stalled. */ + private val tailGraceMs: Long = DEFAULT_TAIL_GRACE_MS, +) { + suspend fun handle( + request: HttpRelayRequest, + response: HttpRelayResponse, + ) { + val command = request.command + val max = server.limits?.maxMessageLength + if (max != null && request.body.size > max) { + return response.single(HttpRelayStatus.PAYLOAD_TOO_LARGE, closed("invalid: the body exceeds $max bytes")) + } + val parsed = + command.parse(request.body.decodeToString()) + ?: return response.single(HttpRelayStatus.BAD_REQUEST, closed("invalid: the body is not ${command.name}'s arguments")) + if (max != null && parsed.wireLength > max) { + return response.single(HttpRelayStatus.PAYLOAD_TOO_LARGE, closed("invalid: the command exceeds $max characters")) + } + val signedIn = + when (val proof = proofOf(request)) { + is Proof.Anonymous -> emptySet() + is Proof.Signed -> setOf(proof.pubkey) + is Proof.Refused -> return response.single(HttpRelayStatus.UNAUTHORIZED, closed(MachineReadablePrefix.AUTH_REQUIRED.format(proof.reason))) + } + exchange(parsed, command, signedIn, response) + } + + /** A frame as queued: its wire text, and its type when the engine built one. */ + private class Frame( + val json: String, + val message: Message?, + ) + + private fun HttpRelayCommand.ends(frame: Frame) = frame.message?.let(::ends) == true + + private suspend fun exchange( + parsed: HttpRelayCommand.Parsed, + command: HttpRelayCommand, + signedIn: Set<HexKey>, + response: HttpRelayResponse, + ) = coroutineScope { + val frames = Channel<Frame>(maxQueuedFrames) + val ended = CompletableDeferred<Unit>() + + // Called on the engine's coroutines and cannot suspend, so a frame that does not fit ends the answer. + fun offer( + frame: Frame, + last: Boolean, + ) { + val sent = frames.trySend(frame) + if (sent.isClosed) return + if (sent.isFailure || last) { + frames.close() + ended.complete(Unit) + } + } + val sink = + object : SessionSink { + override fun message(message: Message) { + // The challenge every connection opens with; this one proved its key by NIP-98 instead. + if (message is AuthMessage) return + offer(Frame(message.toJson(), message), command.ends(message)) + } + + override fun raw(json: String) = offer(Frame(json, null), false) + } + val session = + launch { + server.serve(sink, signedIn) { + it.receive(parsed.command) + ended.await() + } + } + val started = TimeSource.Monotonic.markNow() + + fun remainingMs() = (deadlineMs - started.elapsedNow().inWholeMilliseconds).coerceAtLeast(0) + try { + val first = withTimeoutOrNull(deadlineMs) { frames.receiveCatching().getOrNull() } + when { + first == null -> { + response.single(HttpRelayStatus.UNAVAILABLE, closed("error: no answer within ${deadlineMs / 1000}s")) + } + + command.ends(first) || HttpRelayStatus.of(first.message) != HttpRelayStatus.OK -> { + response.single(HttpRelayStatus.of(first.message), first.json) + } + + else -> { + response.stream { + // The deadline is read between frames and never interrupts a write, so every line leaves + // whole; a reader that stops reading altogether is dropped at the hard stop. + try { + withTimeout(remainingMs() + tailGraceMs) { + when (drain(first, frames, command, started)) { + Ending.ANSWERED -> {} + Ending.DEADLINE -> line(closed("error: the answer ran past ${deadlineMs / 1000}s")) + Ending.CUT -> line(closed("error: slow reader, over $maxQueuedFrames frames waiting")) + } + flush() + } + } catch (_: TimeoutCancellationException) { + throw HttpRelayReaderStalled() + } + } + } + } + } finally { + session.cancel() + } + } + + /** How a streamed answer stopped: at its answer frame, at the deadline, or cut because the reader fell behind. */ + private enum class Ending { ANSWERED, DEADLINE, CUT } + + /** + * Writes [first] and what follows up to the command's answer, flushing whenever nothing is waiting so + * a burst leaves as one write. Stops at the answer: a live event queued behind it is not part of it. + */ + private suspend fun HttpRelayLines.drain( + first: Frame, + frames: Channel<Frame>, + command: HttpRelayCommand, + started: TimeSource.Monotonic.ValueTimeMark, + ): Ending { + var frame = first + while (true) { + line(frame.json) + if (command.ends(frame)) return Ending.ANSWERED + frame = frames.tryReceive().getOrNull() ?: run { + flush() + val leftMs = deadlineMs - started.elapsedNow().inWholeMilliseconds + if (leftMs <= 0) return Ending.DEADLINE + val next = withTimeoutOrNull(leftMs) { frames.receiveCatching() } ?: return Ending.DEADLINE + // Closed with no answer frame in it: the send side gave up on this reader. + next.getOrNull() ?: return Ending.CUT + } + if (started.elapsedNow().inWholeMilliseconds >= deadlineMs) return Ending.DEADLINE + } + } + + /** Who a request acts as. */ + private sealed interface Proof { + data object Anonymous : Proof + + class Signed( + val pubkey: HexKey, + ) : Proof + + class Refused( + val reason: String, + ) : Proof + } + + /** + * A NIP-98 header, checked against the address it names when that is one of [origins], so a token + * signed at the .onion verifies there. It must bind the body's hash: it authorizes one command, once. + * Another scheme (a proxy's Basic, a client's Bearer) is not addressed to the relay and is ignored. + */ + private suspend fun proofOf(request: HttpRelayRequest): Proof { + val header = request.authorization?.trim().orEmpty() + val scheme = Nip98AuthVerifier.SCHEME + if (!header.regionMatches(0, scheme, 0, scheme.length, ignoreCase = true)) return Proof.Anonymous + val token = scheme + header.substring(scheme.length).trim() + val accepted = origins().map { it.trimEnd('/') + request.command.path } + val url = claimedUrl(token)?.takeIf { it in accepted } ?: accepted.firstOrNull() ?: return Proof.Refused("this relay names no url to sign") + return when (val r = verifier.verify(token, "POST", url, request.body)) { + is Nip98AuthVerifier.Result.Verified -> Proof.Signed(r.pubkey) + is Nip98AuthVerifier.Result.Malformed -> Proof.Refused("NIP-98 ${r.reason}") + is Nip98AuthVerifier.Result.Missing -> Proof.Anonymous + } + } + + /** The `u` tag of a NIP-98 token, or null when it does not decode; the verifier then says why. */ + @OptIn(ExperimentalEncodingApi::class) + private fun claimedUrl(token: String): String? = + runCatching { + val json = Base64.decode(token.removePrefix(Nip98AuthVerifier.SCHEME).trim()).decodeToString() + OptimizedJsonMapper + .fromJson(json) + .tags + .firstOrNull { it.size > 1 && it[0] == "u" } + ?.get(1) + }.getOrNull() + + private fun closed(reason: String) = ClosedMessage(HttpRelayCommand.SUB_ID, reason).toJson() + + companion object { + const val DEFAULT_DEADLINE_MS = 30_000L + + /** The websocket's slow-consumer bound in the reference relays. */ + const val DEFAULT_MAX_QUEUED_FRAMES = 8192 + + const val DEFAULT_TAIL_GRACE_MS = 5_000L + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayStatus.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayStatus.kt new file mode 100644 index 0000000000..41f8fb64a2 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayStatus.kt @@ -0,0 +1,65 @@ +/* + * 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.quartz.nipFERelayOverHttp + +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ClosedMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NoticeMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage +import com.vitorpamplona.quartz.nip77Negentropy.NegErrMessage + +/** + * NIP-FE status codes. The status of an answer is decided by its first + * frame: an accepting one is 200 and the answer may stream; a refusal's + * NIP-01 machine-readable prefix picks the code. After the first frame is + * out the status cannot change, so later failures are frames. + */ +object HttpRelayStatus { + const val OK = 200 + const val BAD_REQUEST = 400 + const val UNAUTHORIZED = 401 + const val FORBIDDEN = 403 + const val PAYLOAD_TOO_LARGE = 413 + const val TOO_MANY_REQUESTS = 429 + const val INTERNAL_ERROR = 500 + const val UNAVAILABLE = 503 + + /** The status an answer opening with [message] carries. A raw EVENT frame opens a 200. */ + fun of(message: Message?): Int = + when (message) { + is ClosedMessage -> forReason(message.message) + is NegErrMessage -> forReason(message.reason) + // A duplicate is already stored, which is what the caller asked for, whichever flag the store set. + is OkMessage -> if (message.success || message.message.startsWith("duplicate:")) OK else forReason(message.message) + is NoticeMessage -> BAD_REQUEST + else -> OK + } + + /** The status a NIP-01 machine-readable prefix stands for. */ + fun forReason(reason: String): Int = + when (reason.substringBefore(':', "")) { + "auth-required" -> UNAUTHORIZED + "restricted", "blocked" -> FORBIDDEN + "rate-limited" -> TOO_MANY_REQUESTS + "error" -> INTERNAL_ERROR + else -> BAD_REQUEST + } +} diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt new file mode 100644 index 0000000000..e6837eb856 --- /dev/null +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt @@ -0,0 +1,363 @@ +/* + * 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.quartz.nipFERelayOverHttp + +import com.vitorpamplona.negentropy.Negentropy +import com.vitorpamplona.negentropy.storage.StorageVector +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.MachineReadablePrefix +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.server.RelayServerBase +import com.vitorpamplona.quartz.nip01Core.relay.server.RelayServerListener +import com.vitorpamplona.quartz.nip01Core.relay.server.backend.RequestContext +import com.vitorpamplona.quartz.nip01Core.relay.server.backend.SessionBackend +import com.vitorpamplona.quartz.nip01Core.relay.server.policies.PassThroughPolicy +import com.vitorpamplona.quartz.nip01Core.relay.server.policies.PolicyResult +import com.vitorpamplona.quartz.nip01Core.relay.server.policies.RelayLimits +import com.vitorpamplona.quartz.nip01Core.relay.server.policies.VerifyPolicy +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.store.IEventStore +import com.vitorpamplona.quartz.nip01Core.store.IdAndTime +import com.vitorpamplona.quartz.nip77Negentropy.NegentropySettings +import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.runBlocking +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +/** NIP-FE's handler over a relay engine and an in-memory backend: status, lines, auth, and negentropy rounds. */ +class HttpRelayHandlerTest { + /** Everything the backend holds; a filter naming [STALLED_KIND] never answers, one naming [TRICKLE_KIND] never ends. */ + private class MemoryBackend : SessionBackend { + val events = CopyOnWriteArrayList<Event>() + + override suspend fun query( + ctx: RequestContext, + filters: List<Filter>, + onEach: (Event) -> Unit, + onEose: () -> Unit, + ) { + if (filters.any { it.kinds?.contains(STALLED_KIND) == true }) awaitCancellation() + events.filter { e -> filters.any { it.match(e) } }.forEach(onEach) + if (filters.any { it.kinds?.contains(TRICKLE_KIND) == true }) awaitCancellation() + onEose() + awaitCancellation() + } + + override suspend fun count( + ctx: RequestContext, + filters: List<Filter>, + ) = events.count { e -> filters.any { it.match(e) } } + + override suspend fun submit( + event: Event, + onComplete: (IEventStore.InsertOutcome) -> Unit, + ) { + if (events.none { it.id == event.id }) events += event + onComplete(IEventStore.InsertOutcome.Accepted) + } + + override suspend fun snapshotIdsForNegentropy( + filters: List<Filter>, + maxEntries: Int?, + ) = events.filter { e -> filters.any { it.match(e) } }.map { IdAndTime(it.createdAt, it.id) } + } + + /** Refuses every read from a connection nobody signed in on, as an AUTH-gated relay does. */ + private class SignedInOnly : PassThroughPolicy() { + @Volatile private var scope: RequestContext? = null + + override fun onConnect( + scope: RequestContext, + send: (Message) -> Unit, + ) { + this.scope = scope + } + + override fun accept(cmd: ReqCmd): PolicyResult<ReqCmd> = + if (scope?.authenticatedUsers?.isNotEmpty() == true) { + PolicyResult.Accepted(cmd) + } else { + PolicyResult.Rejected(MachineReadablePrefix.AUTH_REQUIRED.format("sign in")) + } + } + + private class MemoryRelay( + override val backend: MemoryBackend, + signedInOnly: Boolean, + ) : RelayServerBase( + policyBuilder = { if (signedInOnly) VerifyPolicy + SignedInOnly() else VerifyPolicy }, + parentContext = SupervisorJob(), + negentropySettings = NegentropySettings.Default, + listener = RelayServerListener.None, + limits = RelayLimits(maxMessageLength = 4096), + ) + + /** One answer as the host would see it. */ + private class Recorded : HttpRelayResponse { + var status = 0 + val lines = mutableListOf<String>() + var streamed = false + + override suspend fun single( + status: Int, + frame: String, + ) { + this.status = status + lines += frame + } + + override suspend fun stream(lines: suspend HttpRelayLines.() -> Unit) { + status = 200 + streamed = true + val out = this.lines + object : HttpRelayLines { + override suspend fun line(frame: String) { + out += frame + } + + override suspend fun flush() {} + }.lines() + } + } + + private val origin = "https://relay.example" + private val alice = NostrSignerSync() + private val backend = MemoryBackend() + + private fun handler( + signedInOnly: Boolean = false, + deadlineMs: Long = 5_000, + ) = HttpRelayHandler(MemoryRelay(backend, signedInOnly), origins = { listOf(origin) }, deadlineMs = deadlineMs) + + private fun HttpRelayHandler.ask( + command: HttpRelayCommand, + body: String, + authorization: String? = null, + ) = Recorded().also { runBlocking { handle(HttpRelayRequest(command, authorization, body.encodeToByteArray()), it) } } + + private fun note( + content: String, + at: Long = 1_700_000_000L, + ) = alice.sign<Event>(at, 1, emptyArray(), content) + + private fun token( + command: HttpRelayCommand, + body: String, + ) = alice.sign(HTTPAuthorizationEvent.build(origin + command.path, "POST", body.encodeToByteArray(), System.currentTimeMillis() / 1000) {}).toAuthToken() + + @Test + fun aReqStreamsItsEventsAndEndsOnEose() { + val a = note("a") + val b = note("b") + backend.events += listOf(a, b) + val answer = handler().ask(HttpRelayCommand.REQ, """{"kinds":[1]}""") + assertEquals(200, answer.status) + assertTrue(answer.streamed) + assertEquals("""["EOSE","http"]""", answer.lines.last()) + assertEquals( + setOf(a.id, b.id), + answer.lines + .dropLast(1) + .map { l -> listOf(a, b).first { it.id in l }.id } + .toSet(), + ) + } + + @Test + fun anEmptyReqIsOneEoseLine() { + val answer = handler().ask(HttpRelayCommand.REQ, """[{"kinds":[30000]}]""") + assertEquals(200, answer.status) + assertEquals(listOf("""["EOSE","http"]"""), answer.lines) + } + + @Test + fun anEventIsAnsweredByItsOkAndAForgeryIsRefused() { + val posted = note("posted") + val ok = handler().ask(HttpRelayCommand.EVENT, posted.toJson()) + assertEquals(200, ok.status) + assertEquals(listOf("""["OK","${posted.id}",true,""]"""), ok.lines) + assertTrue(backend.events.any { it.id == posted.id }) + + val forged = Event(posted.id, posted.pubKey, posted.createdAt, posted.kind, posted.tags, "tampered", posted.sig) + val refused = handler().ask(HttpRelayCommand.EVENT, forged.toJson()) + assertEquals(400, refused.status, refused.lines.toString()) + assertTrue(refused.lines.single().startsWith("""["OK","${forged.id}",false,"""), refused.lines.toString()) + } + + @Test + fun aCountIsOneCountLine() { + backend.events += listOf(note("a"), note("b"), note("c")) + val answer = handler().ask(HttpRelayCommand.COUNT, """{"kinds":[1]}""") + assertEquals(200, answer.status) + assertTrue(answer.lines.single().startsWith("""["COUNT","http",{"count":3"""), answer.lines.toString()) + } + + @Test + fun aBodyThatIsNotTheCommandsArgumentsIsA400AndOneOverTheLimitA413() { + for ((command, body) in listOf( + HttpRelayCommand.REQ to "[]", + HttpRelayCommand.EVENT to """[{"id":"x"}]""", + HttpRelayCommand.EVENT to """{"id":"not an event"}""", + HttpRelayCommand.NEG to """[{"kinds":[1]}]""", + HttpRelayCommand.COUNT to "not json", + )) { + val answer = handler().ask(command, body) + assertEquals(400, answer.status, "$command '$body'") + assertTrue(answer.lines.single().startsWith("""["CLOSED","http","invalid:"""), answer.lines.toString()) + } + assertEquals(413, handler().ask(HttpRelayCommand.REQ, """{"search":"${"x".repeat(5_000)}"}""").status) + // Under the byte cap, over it once wrapped in its frame: the engine measures the frame. + assertEquals(413, handler().ask(HttpRelayCommand.REQ, """{"search":"${"x".repeat(4_096 - 20)}"}""").status) + } + + @Test + fun aNip98SignatureSignsTheSessionInAndAnotherSchemeDoesNot() { + backend.events += note("gated") + val gated = handler(signedInOnly = true) + val body = """{"kinds":[1]}""" + + val anonymous = gated.ask(HttpRelayCommand.REQ, body) + assertEquals(401, anonymous.status) + assertTrue(anonymous.lines.single().startsWith("""["CLOSED","http","auth-required:""")) + + assertEquals(401, gated.ask(HttpRelayCommand.REQ, body, "Basic dXNlcjpwYXNz").status, "Basic is not addressed to the relay") + + val signed = gated.ask(HttpRelayCommand.REQ, body, token(HttpRelayCommand.REQ, body)) + assertEquals(200, signed.status, signed.lines.toString()) + assertEquals("""["EOSE","http"]""", signed.lines.last()) + } + + @Test + fun aTokenForAnotherBodyOrASecondUseIsRefused() { + val h = handler() + val body = """{"kinds":[1]}""" + val once = token(HttpRelayCommand.REQ, body) + assertEquals(200, h.ask(HttpRelayCommand.REQ, body, once).status) + val replayed = h.ask(HttpRelayCommand.REQ, body, once) + assertEquals(401, replayed.status) + assertTrue("replay" in replayed.lines.single(), replayed.lines.toString()) + val other = h.ask(HttpRelayCommand.REQ, """{"kinds":[0]}""", token(HttpRelayCommand.REQ, body)) + assertEquals(401, other.status) + assertTrue("payload" in other.lines.single(), other.lines.toString()) + } + + @Test + fun negentropyReconcilesInStatelessRounds() { + val shared = (1..30).map { note("shared $it", 1_700_000_000L + it) } + val onlyRelay = (1..12).map { note("relay $it", 1_700_001_000L + it) } + val onlyClient = (1..7).map { note("client $it", 1_700_002_000L + it) } + backend.events += shared + onlyRelay + + val mine = StorageVector().apply { (shared + onlyClient).forEach { insert(it.createdAt, it.id) } }.also { it.seal() } + val client = Negentropy(mine, 0) + var message = client.initiate().toHexKey() + val have = mutableSetOf<String>() + val need = mutableSetOf<String>() + var rounds = 0 + while (true) { + check(++rounds < 20) { "no convergence" } + // A fresh handler each round: nothing on the server side carries over. + val answer = handler().ask(HttpRelayCommand.NEG, """[{"kinds":[1]},"$message"]""") + assertEquals(200, answer.status, answer.lines.toString()) + val reply = + answer.lines + .single() + .substringAfter("""["NEG-MSG","http","""") + .substringBefore('"') + val result = client.reconcile(reply.hexToByteArray()) + have += result.sendIds.map { it.toHexString() } + need += result.needIds.map { it.toHexString() } + message = result.msg?.toHexKey() ?: break + } + assertEquals(onlyClient.map { it.id }.toSet(), have) + assertEquals(onlyRelay.map { it.id }.toSet(), need) + } + + @Test + fun aMalformedNegentropyRoundIsRefused() { + val answer = handler().ask(HttpRelayCommand.NEG, """[{"kinds":[1]},"zz"]""") + assertEquals(400, answer.status) + assertTrue(answer.lines.single().startsWith("""["NEG-ERR","http","""), answer.lines.toString()) + } + + @Test + fun noFirstFrameWithinTheDeadlineIsA503() { + val answer = handler(deadlineMs = 300).ask(HttpRelayCommand.REQ, """{"kinds":[$STALLED_KIND]}""") + assertEquals(503, answer.status) + assertTrue(answer.lines.single().startsWith("""["CLOSED","http","error: no answer""")) + } + + @Test + fun aDeadlineMidAnswerEndsOnAClosedLine() { + val found = note("found") + backend.events += found + val answer = handler(deadlineMs = 300).ask(HttpRelayCommand.REQ, """{"kinds":[1,$TRICKLE_KIND]}""") + assertEquals(200, answer.status) + assertTrue(found.id in answer.lines.first()) + assertTrue(answer.lines.last().startsWith("""["CLOSED","http","error: the answer ran past"""), answer.lines.toString()) + } + + @Test + fun aReaderThatStopsReadingIsDroppedAtTheHardStop() { + backend.events += note("found") + val h = HttpRelayHandler(MemoryRelay(backend, false), origins = { listOf(origin) }, deadlineMs = 100, tailGraceMs = 100) + val stalled = + object : HttpRelayResponse { + override suspend fun single( + status: Int, + frame: String, + ) = error("streams") + + override suspend fun stream(lines: suspend HttpRelayLines.() -> Unit) = + object : HttpRelayLines { + override suspend fun line(frame: String) = awaitCancellation() + + override suspend fun flush() {} + }.lines() + } + assertFailsWith<HttpRelayReaderStalled> { + runBlocking { h.handle(HttpRelayRequest(HttpRelayCommand.REQ, null, """{"kinds":[1,$TRICKLE_KIND]}""".encodeToByteArray()), stalled) } + } + } + + @Test + fun theStringOnlyConnectStillSendsWireJson() { + backend.events += note("socket") + val out = CopyOnWriteArrayList<String>() + val session = MemoryRelay(backend, false).connect { out += it } + runBlocking { session.receive("""["REQ","s",{"kinds":[1]}]""") } + assertTrue(out.any { it.startsWith("""["EVENT","s",""") } && out.contains("""["EOSE","s"]"""), out.toString()) + session.close() + } + + private companion object { + const val STALLED_KIND = 7 + const val TRICKLE_KIND = 8 + } +} From 3b019b45d0dd055eb02deb27d14bdcf354715ca3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona <vitor@vitorpamplona.com> Date: Sat, 26 Sep 2026 22:41:40 -0400 Subject: [PATCH 31/60] fix(browser): the pill's site-settings line only told the truth after you opened page info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sitePermissions` was filled in exactly one place — inside `showPageInfo`, from the answer the broker sends when the sheet is about to open. So a freshly opened window said "Nothing allowed yet" about a site it had already been granted the camera on, and only stopped saying it once you opened the sheet that made it ask. Page info itself was always right; the summary above it was not. The registry lives in the main process and does not call back, so the window has to ask. It now asks when the origin changes, which is when the answer can differ, and keeps the last origin so a same-site navigation is not a round trip per page. An edit made in the sheet also folds straight into the summary. The registry stays authoritative — this only stops the line behind the sheet disagreeing with the sheet in front of it until the next navigation. Found while testing "Open full screen": that window reported "Nothing allowed yet" for brainstorm.world while its own page info showed Camera = Allow. Verified on an SM-T220: the line now reads "Camera allowed" right after the edit, and again in a brand-new window without opening page info at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- .../napplethost/NappletBrowserActivity.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt index a32f8f27d1..4525300d1b 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt @@ -155,6 +155,9 @@ class NappletBrowserActivity : ComponentActivity() { // same address don't each trigger a round-trip. private var lastFavoriteQueryUrl: String? = null + /** The origin whose site permissions the pill is already showing. */ + private var lastSitePermissionOrigin: String? = null + // What Recents shows for this task: the page's title, favicon and theme colour. private var pageTitle: String? = null private var pageIcon: Bitmap? = null @@ -771,6 +774,20 @@ class NappletBrowserActivity : ComponentActivity() { copy(chrome = chrome.copy(url = url), title = if (newSite) BrowserChrome.displayHost(url) else title, isFavorite = false) } } + // The pill's "Site settings" line summarises this origin's camera/mic/location answers, and the + // registry that holds them lives in the main process. Until this was asked for on navigation, the + // only thing that ever filled it was opening page info — so a window said "Nothing allowed yet" + // about a site it had already been granted the camera on, and only stopped lying once you opened + // the sheet that made it ask. + BrowserChrome.originOf(url)?.let { origin -> + if (origin != lastSitePermissionOrigin) { + lastSitePermissionOrigin = origin + querySitePermissions(origin) { decisions -> + updateUi { copy(sitePermissions = decisions.filterValues { it != Decision.ASK }) } + } + } + } + if (url == lastFavoriteQueryUrl) return lastFavoriteQueryUrl = url val msg = @@ -1383,6 +1400,14 @@ class NappletBrowserActivity : ComponentActivity() { decision: Decision, ) { BrowserChrome.originOf(currentUrl())?.let { rememberSitePermission(it, permission, decision) } + // Keep the pill's summary in step with the sheet the change was made in. The registry + // is authoritative and lives in the other process, but it does not call back, so an + // edit made here would otherwise not show up until the next navigation. + updateUi { + val next = sitePermissions.toMutableMap() + if (decision == Decision.ASK) next.remove(permission) else next[permission] = decision + copy(sitePermissions = next) + } } override fun onClearSiteData() { From 741d8e9cdbf9fafc74544b8fd0934b5d2364ee4c Mon Sep 17 00:00:00 2001 From: vitorpamplona <532031+vitorpamplona@users.noreply.github.com> Date: Sun, 27 Sep 2026 02:46:51 +0000 Subject: [PATCH 32/60] chore: sync Crowdin translations and seed translator npub placeholders --- .../composeResources/values-ar-rSA/strings.xml | 1 + .../composeResources/values-bn-rBD/strings.xml | 1 + .../composeResources/values-cs/strings.xml | 1 + .../composeResources/values-de-rDE/strings.xml | 1 + .../composeResources/values-el-rGR/strings.xml | 1 + .../composeResources/values-en-rGB/strings.xml | 1 + .../composeResources/values-eo-rUY/strings.xml | 1 + .../composeResources/values-es-rES/strings.xml | 1 + .../composeResources/values-es-rMX/strings.xml | 1 + .../composeResources/values-es-rUS/strings.xml | 1 + .../composeResources/values-fa-rIR/strings.xml | 1 + .../composeResources/values-fi-rFI/strings.xml | 1 + .../composeResources/values-fr-rCA/strings.xml | 1 + .../composeResources/values-fr-rFR/strings.xml | 1 + .../composeResources/values-hi-rIN/strings.xml | 1 + .../composeResources/values-hu-rHU/strings.xml | 1 + .../composeResources/values-in-rID/strings.xml | 1 + .../composeResources/values-it-rIT/strings.xml | 1 + .../composeResources/values-ja-rJP/strings.xml | 1 + .../composeResources/values-ko-rKR/strings.xml | 1 + .../composeResources/values-lv-rLV/strings.xml | 1 + .../composeResources/values-nl-rNL/strings.xml | 1 + .../composeResources/values-pl-rPL/strings.xml | 1 + .../composeResources/values-pt-rBR/strings.xml | 1 + .../composeResources/values-pt-rPT/strings.xml | 1 + .../composeResources/values-ru-rRU/strings.xml | 1 + .../composeResources/values-ru-rUA/strings.xml | 1 + .../composeResources/values-sl-rSI/strings.xml | 1 + .../composeResources/values-sr-rSP/strings.xml | 1 + .../composeResources/values-sv-rSE/strings.xml | 1 + .../composeResources/values-sw/strings.xml | 1 + .../composeResources/values-ta-rIN/strings.xml | 1 + .../composeResources/values-th-rTH/strings.xml | 1 + .../composeResources/values-tr-rTR/strings.xml | 1 + .../composeResources/values-uk-rUA/strings.xml | 1 + .../composeResources/values-uz-rUZ/strings.xml | 1 + .../composeResources/values-vi-rVN/strings.xml | 1 + .../composeResources/values-zh-rCN/strings.xml | 1 + .../composeResources/values-zh-rHK/strings.xml | 1 + .../composeResources/values-zh-rSG/strings.xml | 1 + .../composeResources/values-zh-rTW/strings.xml | 1 + docs/changelog/translators.json | 12 ++++++------ 42 files changed, 47 insertions(+), 6 deletions(-) diff --git a/commonsUI/src/commonMain/composeResources/values-ar-rSA/strings.xml b/commonsUI/src/commonMain/composeResources/values-ar-rSA/strings.xml index c9b3131eff..e9970306ff 100644 --- a/commonsUI/src/commonMain/composeResources/values-ar-rSA/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ar-rSA/strings.xml @@ -3081,4 +3081,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-bn-rBD/strings.xml b/commonsUI/src/commonMain/composeResources/values-bn-rBD/strings.xml index 75664b26ee..b34056a786 100644 --- a/commonsUI/src/commonMain/composeResources/values-bn-rBD/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-bn-rBD/strings.xml @@ -2956,4 +2956,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-cs/strings.xml b/commonsUI/src/commonMain/composeResources/values-cs/strings.xml index b21d6ee05a..3f818c83b8 100644 --- a/commonsUI/src/commonMain/composeResources/values-cs/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-cs/strings.xml @@ -5139,4 +5139,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-de-rDE/strings.xml b/commonsUI/src/commonMain/composeResources/values-de-rDE/strings.xml index 17617a8ae7..94482ed9f7 100644 --- a/commonsUI/src/commonMain/composeResources/values-de-rDE/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-de-rDE/strings.xml @@ -4923,4 +4923,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-el-rGR/strings.xml b/commonsUI/src/commonMain/composeResources/values-el-rGR/strings.xml index ffe6a08835..2fe2f7373b 100644 --- a/commonsUI/src/commonMain/composeResources/values-el-rGR/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-el-rGR/strings.xml @@ -2904,4 +2904,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-en-rGB/strings.xml b/commonsUI/src/commonMain/composeResources/values-en-rGB/strings.xml index 44a5f2359e..fa8183a377 100644 --- a/commonsUI/src/commonMain/composeResources/values-en-rGB/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-en-rGB/strings.xml @@ -34,4 +34,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-eo-rUY/strings.xml b/commonsUI/src/commonMain/composeResources/values-eo-rUY/strings.xml index 35481e4172..ce975e7024 100644 --- a/commonsUI/src/commonMain/composeResources/values-eo-rUY/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-eo-rUY/strings.xml @@ -2960,4 +2960,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-es-rES/strings.xml b/commonsUI/src/commonMain/composeResources/values-es-rES/strings.xml index 45b069c10c..3035a49c08 100644 --- a/commonsUI/src/commonMain/composeResources/values-es-rES/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-es-rES/strings.xml @@ -2976,4 +2976,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-es-rMX/strings.xml b/commonsUI/src/commonMain/composeResources/values-es-rMX/strings.xml index d941f9ea99..791a4dc908 100644 --- a/commonsUI/src/commonMain/composeResources/values-es-rMX/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-es-rMX/strings.xml @@ -2969,4 +2969,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-es-rUS/strings.xml b/commonsUI/src/commonMain/composeResources/values-es-rUS/strings.xml index 3e6060ab9c..23d497b889 100644 --- a/commonsUI/src/commonMain/composeResources/values-es-rUS/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-es-rUS/strings.xml @@ -2969,4 +2969,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-fa-rIR/strings.xml b/commonsUI/src/commonMain/composeResources/values-fa-rIR/strings.xml index df5f3b4363..3ecb2f982d 100644 --- a/commonsUI/src/commonMain/composeResources/values-fa-rIR/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-fa-rIR/strings.xml @@ -2970,4 +2970,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-fi-rFI/strings.xml b/commonsUI/src/commonMain/composeResources/values-fi-rFI/strings.xml index f2784cced4..b9116115b5 100644 --- a/commonsUI/src/commonMain/composeResources/values-fi-rFI/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-fi-rFI/strings.xml @@ -2936,4 +2936,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-fr-rCA/strings.xml b/commonsUI/src/commonMain/composeResources/values-fr-rCA/strings.xml index 677d4afb32..e31bf3e41c 100644 --- a/commonsUI/src/commonMain/composeResources/values-fr-rCA/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-fr-rCA/strings.xml @@ -2860,4 +2860,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-fr-rFR/strings.xml b/commonsUI/src/commonMain/composeResources/values-fr-rFR/strings.xml index 76a4f93daf..15fe140875 100644 --- a/commonsUI/src/commonMain/composeResources/values-fr-rFR/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-fr-rFR/strings.xml @@ -3153,4 +3153,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml b/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml index cde8a721e1..ab38725872 100644 --- a/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml @@ -5263,6 +5263,7 @@ <string name="cyberspace_bag_unsigned">हस्ताक्षररहित। इसलिए यह कर्ता केवल एक कथन है</string> <string name="cyberspace_bag_unknown_kind">प्रकार %1$d का वस्तु। जिसे यह ग्राहक चित्रित नहीं करता</string> <string name="cyberspace_bag_opaque">%1$d अष्टकों का अमुक जिसे यह ग्राहक पढता नहीं</string> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> <string name="backup_action_block_again">पुनःबाधित करें %1$s को</string> <string name="backup_action_keep_count">रखें %1$s</string> <string name="backup_action_rejoin">पुनःजुडें %1$s</string> diff --git a/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml b/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml index f7872f3f26..bc0ba670c6 100644 --- a/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml @@ -5263,6 +5263,7 @@ <string name="cyberspace_bag_unsigned">Aláírás nélküli, így ez a szerző csak állítás</string> <string name="cyberspace_bag_unknown_kind">Egy %1$d típusú elem, amelyet ez a kliens nem jelenít meg</string> <string name="cyberspace_bag_opaque">%1$d bájt valamiből, amit ez a kliens nem olvas</string> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> <string name="backup_action_block_again">%1$s újbóli letiltása</string> <string name="backup_action_keep_count">%1$s megtartása</string> <string name="backup_action_rejoin">Újbóli csatlakozás ehhez: %1$s</string> diff --git a/commonsUI/src/commonMain/composeResources/values-in-rID/strings.xml b/commonsUI/src/commonMain/composeResources/values-in-rID/strings.xml index 3ce5510f41..9ff7d96b62 100644 --- a/commonsUI/src/commonMain/composeResources/values-in-rID/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-in-rID/strings.xml @@ -2862,4 +2862,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-it-rIT/strings.xml b/commonsUI/src/commonMain/composeResources/values-it-rIT/strings.xml index 54245dec39..e097e17c7f 100644 --- a/commonsUI/src/commonMain/composeResources/values-it-rIT/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-it-rIT/strings.xml @@ -2900,4 +2900,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-ja-rJP/strings.xml b/commonsUI/src/commonMain/composeResources/values-ja-rJP/strings.xml index 089c9f9857..61af0a4940 100644 --- a/commonsUI/src/commonMain/composeResources/values-ja-rJP/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ja-rJP/strings.xml @@ -2920,4 +2920,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-ko-rKR/strings.xml b/commonsUI/src/commonMain/composeResources/values-ko-rKR/strings.xml index 4e876d3cf0..68998b3497 100644 --- a/commonsUI/src/commonMain/composeResources/values-ko-rKR/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ko-rKR/strings.xml @@ -2904,4 +2904,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-lv-rLV/strings.xml b/commonsUI/src/commonMain/composeResources/values-lv-rLV/strings.xml index 2cb28ecf8f..0c9953b12e 100644 --- a/commonsUI/src/commonMain/composeResources/values-lv-rLV/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-lv-rLV/strings.xml @@ -2988,4 +2988,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-nl-rNL/strings.xml b/commonsUI/src/commonMain/composeResources/values-nl-rNL/strings.xml index 3ea2d52fc0..a47c701a37 100644 --- a/commonsUI/src/commonMain/composeResources/values-nl-rNL/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-nl-rNL/strings.xml @@ -4632,4 +4632,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-pl-rPL/strings.xml b/commonsUI/src/commonMain/composeResources/values-pl-rPL/strings.xml index 570f8fc2b1..ccd2e12d38 100644 --- a/commonsUI/src/commonMain/composeResources/values-pl-rPL/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-pl-rPL/strings.xml @@ -5485,6 +5485,7 @@ <string name="cyberspace_bag_unsigned">Brak podpisu, więc autor to tylko domysł</string> <string name="cyberspace_bag_unknown_kind">Element typu %1$d, którego ten klient nie wyświetla</string> <string name="cyberspace_bag_opaque">%1$d bajtów danych, których ten klient nie odczytuje</string> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> <string name="backup_action_block_again">Zablokuj %1$s ponownie</string> <string name="backup_action_keep_count">Zachowaj %1$s</string> <string name="backup_action_rejoin">Ponów %1$s</string> diff --git a/commonsUI/src/commonMain/composeResources/values-pt-rBR/strings.xml b/commonsUI/src/commonMain/composeResources/values-pt-rBR/strings.xml index 1a156efffd..fdc35e9309 100644 --- a/commonsUI/src/commonMain/composeResources/values-pt-rBR/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-pt-rBR/strings.xml @@ -4921,4 +4921,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-pt-rPT/strings.xml b/commonsUI/src/commonMain/composeResources/values-pt-rPT/strings.xml index d722a0814b..6df337c8f7 100644 --- a/commonsUI/src/commonMain/composeResources/values-pt-rPT/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-pt-rPT/strings.xml @@ -2920,4 +2920,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-ru-rRU/strings.xml b/commonsUI/src/commonMain/composeResources/values-ru-rRU/strings.xml index d89352fd93..2d9214c475 100644 --- a/commonsUI/src/commonMain/composeResources/values-ru-rRU/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ru-rRU/strings.xml @@ -3007,4 +3007,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-ru-rUA/strings.xml b/commonsUI/src/commonMain/composeResources/values-ru-rUA/strings.xml index 4b2c8833b7..4656ddd56e 100644 --- a/commonsUI/src/commonMain/composeResources/values-ru-rUA/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ru-rUA/strings.xml @@ -2988,4 +2988,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-sl-rSI/strings.xml b/commonsUI/src/commonMain/composeResources/values-sl-rSI/strings.xml index 8ae6e95125..94bd73c074 100644 --- a/commonsUI/src/commonMain/composeResources/values-sl-rSI/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-sl-rSI/strings.xml @@ -5007,4 +5007,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-sr-rSP/strings.xml b/commonsUI/src/commonMain/composeResources/values-sr-rSP/strings.xml index 9aee9b1e4d..79291d0563 100644 --- a/commonsUI/src/commonMain/composeResources/values-sr-rSP/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-sr-rSP/strings.xml @@ -2965,4 +2965,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-sv-rSE/strings.xml b/commonsUI/src/commonMain/composeResources/values-sv-rSE/strings.xml index 0b434776f0..64cf4922d4 100644 --- a/commonsUI/src/commonMain/composeResources/values-sv-rSE/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-sv-rSE/strings.xml @@ -4921,4 +4921,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-sw/strings.xml b/commonsUI/src/commonMain/composeResources/values-sw/strings.xml index 439b9c7be0..a5f776dbef 100644 --- a/commonsUI/src/commonMain/composeResources/values-sw/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-sw/strings.xml @@ -2940,4 +2940,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-ta-rIN/strings.xml b/commonsUI/src/commonMain/composeResources/values-ta-rIN/strings.xml index 106be142e3..4b61e32475 100644 --- a/commonsUI/src/commonMain/composeResources/values-ta-rIN/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-ta-rIN/strings.xml @@ -2929,4 +2929,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-th-rTH/strings.xml b/commonsUI/src/commonMain/composeResources/values-th-rTH/strings.xml index ae6a8e1778..fccf86948f 100644 --- a/commonsUI/src/commonMain/composeResources/values-th-rTH/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-th-rTH/strings.xml @@ -2770,4 +2770,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-tr-rTR/strings.xml b/commonsUI/src/commonMain/composeResources/values-tr-rTR/strings.xml index 59fd97ae80..f73a393d7d 100644 --- a/commonsUI/src/commonMain/composeResources/values-tr-rTR/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-tr-rTR/strings.xml @@ -2936,4 +2936,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-uk-rUA/strings.xml b/commonsUI/src/commonMain/composeResources/values-uk-rUA/strings.xml index 5e60c8e1da..334b4cb8fd 100644 --- a/commonsUI/src/commonMain/composeResources/values-uk-rUA/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-uk-rUA/strings.xml @@ -2994,4 +2994,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-uz-rUZ/strings.xml b/commonsUI/src/commonMain/composeResources/values-uz-rUZ/strings.xml index 3c8e9daa2a..1584c9d77e 100644 --- a/commonsUI/src/commonMain/composeResources/values-uz-rUZ/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-uz-rUZ/strings.xml @@ -2945,4 +2945,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-vi-rVN/strings.xml b/commonsUI/src/commonMain/composeResources/values-vi-rVN/strings.xml index e39e556680..9a29f36685 100644 --- a/commonsUI/src/commonMain/composeResources/values-vi-rVN/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-vi-rVN/strings.xml @@ -2890,4 +2890,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-zh-rCN/strings.xml b/commonsUI/src/commonMain/composeResources/values-zh-rCN/strings.xml index 36527eeeaf..993bad59a6 100644 --- a/commonsUI/src/commonMain/composeResources/values-zh-rCN/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-zh-rCN/strings.xml @@ -3906,4 +3906,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-zh-rHK/strings.xml b/commonsUI/src/commonMain/composeResources/values-zh-rHK/strings.xml index 4ee4682e0a..af8d37327a 100644 --- a/commonsUI/src/commonMain/composeResources/values-zh-rHK/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-zh-rHK/strings.xml @@ -2955,4 +2955,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-zh-rSG/strings.xml b/commonsUI/src/commonMain/composeResources/values-zh-rSG/strings.xml index 0d0a9f7f9e..5d8c0b7ca4 100644 --- a/commonsUI/src/commonMain/composeResources/values-zh-rSG/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-zh-rSG/strings.xml @@ -2955,4 +2955,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-zh-rTW/strings.xml b/commonsUI/src/commonMain/composeResources/values-zh-rTW/strings.xml index 65c10efd5d..8f327fa1bf 100644 --- a/commonsUI/src/commonMain/composeResources/values-zh-rTW/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-zh-rTW/strings.xml @@ -2926,4 +2926,5 @@ <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> </resources> diff --git a/docs/changelog/translators.json b/docs/changelog/translators.json index 018e4cd32a..18d1805a8c 100644 --- a/docs/changelog/translators.json +++ b/docs/changelog/translators.json @@ -123,18 +123,18 @@ "Vietnamese" ] }, - { - "user": "rajs19420616", - "languages": [ - "Hindi" - ] - }, { "user": "maxblake2015", "languages": [ "Polish" ] }, + { + "user": "rajs19420616", + "languages": [ + "Hindi" + ] + }, { "user": "summoner001", "languages": [ From 4d5fe4bbdb83b70fdbe23c96031cd943bc7f9ffa Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sun, 27 Sep 2026 02:49:25 +0000 Subject: [PATCH 33/60] fix(quartz): NIP-98 replay cache refuses when full instead of forgetting a live token The verifier's replay cache evicted its oldest entry once it held 1024, live or not. On a public endpoint that is a replay primitive: sign 1024 throwaway tokens and a captured one inside its 60s window verifies again (reproduced against NIP-FE's /req). Entries now leave only by expiry; a cache full of live tokens answers the new one `rate-limited:` instead. The cap is a constructor parameter (default unchanged, 1024, sized for the admin rpc), so a public endpoint sizes it for its own signed-request rate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jDNhr6a3J4VC5TaG3Yd48 --- .../quartz/nip98HttpAuth/Nip98AuthVerifier.kt | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifier.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifier.kt index ebd580ee15..da83ca0d6d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifier.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifier.kt @@ -52,10 +52,16 @@ class Nip98AuthVerifier( private val now: () -> Long = { TimeUtils.now() }, /** Allowed clock skew in seconds. NIP-98 says 60. */ private val toleranceSeconds: Long = 60, + /** + * Tokens remembered at once. When every remembered token is still inside its window, a new one + * is refused (`rate-limited:`) rather than an unexpired one forgotten: forgetting is what lets a + * captured token be replayed. Size it for the endpoint's signed-request rate over 2 x tolerance. + */ + private val maxReplayEntries: Int = MAX_REPLAY_ENTRIES, ) { /** * Recently-accepted event ids → expiry epoch second. Bounded to - * [MAX_REPLAY_ENTRIES] (insertion-order eviction); each entry expires + * [maxReplayEntries], never by evicting a live entry; each entry expires * after `2 × toleranceSeconds` (twice the accepted window so a token * can't be reused by an attacker who buffers across the boundary). * @@ -140,18 +146,13 @@ class Nip98AuthVerifier( while (it.hasNext()) { if (it.next().value <= nowSec) it.remove() else break } - if (seenEventIds.put(event.id, expiry) != null) { + if (seenEventIds.containsKey(event.id)) { return Result.Malformed("replay: this NIP-98 token has already been used") } - // Cap entries: drop oldest by insertion order. Equivalent to - // the JDK LinkedHashMap.removeEldestEntry hook we used before, - // but works in KMP commonMain. - while (seenEventIds.size > MAX_REPLAY_ENTRIES) { - val eldest = seenEventIds.keys.iterator() - if (!eldest.hasNext()) break - eldest.next() - eldest.remove() + if (seenEventIds.size >= maxReplayEntries) { + return Result.Malformed(REPLAY_CACHE_FULL) } + seenEventIds[event.id] = expiry } return Result.Verified(event.pubKey) @@ -173,11 +174,13 @@ class Nip98AuthVerifier( const val SCHEME = "Nostr " /** - * Cap on the in-memory replay-cache size. With a 60s tolerance - * an attacker would need to push >MAX/120 verified requests per - * second (one new id per ~120 ms) to evict legitimate entries. - * 1024 is generous for an admin endpoint. + * Default cap on the in-memory replay cache: with a 60s tolerance, 1024 live tokens is about + * 8 signed requests a second, generous for an admin endpoint. A public endpoint passes a + * larger `maxReplayEntries`. */ const val MAX_REPLAY_ENTRIES = 1024 + + /** Answered when the replay cache holds nothing but live tokens. */ + const val REPLAY_CACHE_FULL = "rate-limited: too many fresh NIP-98 tokens at once; retry shortly" } } From aef9f6f9a0a885bdb9c0b8063d496271c8a576ac Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sun, 27 Sep 2026 02:49:26 +0000 Subject: [PATCH 34/60] perf(quartz): keep NIP-77 snapshots per filter until a write can change their set LiveEventStore cached one sealed negentropy snapshot and dropped it on any accepted write. On a relay taking writes that meant a full scan + O(n log n) seal on nearly every NEG-OPEN, and NIP-FE's stateless /neg rounds re-open per round, so a reconcile paid it once per round instead of once per sync. The cache now keeps the last four filters (most recently used first) and drops an entry only when an accepted write can change its set: the event matches one of its filters; it is a replaceable or addressable event in the filter's kinds/authors (its predecessor may be in the set under tags or ids the new version lacks); or it is a deletion or vanish request (clears all). Deletes that bypass ingest stay bounded by the existing 30s TTL. The write generation counter is gone. LiveEventStoreSnapshotCacheTest pins each case against a counting store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jDNhr6a3J4VC5TaG3Yd48 --- .../relay/server/backend/LiveEventStore.kt | 86 +++++++---- .../LiveEventStoreSnapshotCacheTest.kt | 133 ++++++++++++++++++ 2 files changed, 189 insertions(+), 30 deletions(-) create mode 100644 quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/LiveEventStoreSnapshotCacheTest.kt diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/LiveEventStore.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/LiveEventStore.kt index 2c37a5b904..1569cc194f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/LiveEventStore.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/LiveEventStore.kt @@ -22,18 +22,21 @@ package com.vitorpamplona.quartz.nip01Core.relay.server.backend import com.vitorpamplona.negentropy.storage.IStorage import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.isAddressable +import com.vitorpamplona.quartz.nip01Core.core.isReplaceable import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterIndex import com.vitorpamplona.quartz.nip01Core.store.IEventStore import com.vitorpamplona.quartz.nip01Core.store.IdAndTime import com.vitorpamplona.quartz.nip01Core.store.RawEvent import com.vitorpamplona.quartz.nip01Core.store.StoreQueryContext +import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent +import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.withContext import kotlin.concurrent.atomics.AtomicBoolean -import kotlin.concurrent.atomics.AtomicLong import kotlin.concurrent.atomics.AtomicReference import kotlin.concurrent.atomics.ExperimentalAtomicApi @@ -154,7 +157,7 @@ class LiveEventStore( ) { ingest.submit(event, skipVerify) { outcome -> if (outcome is IEventStore.InsertOutcome.Accepted) { - writeGeneration.addAndFetch(1L) + forgetSnapshotsCovering(event) fanout(event) } onComplete(outcome) @@ -388,32 +391,28 @@ class LiveEventStore( // ------------------------------------------------------------------ /** - * Bumped after every accepted write. A cached negentropy snapshot is - * only valid while this hasn't moved. Deletion paths that bypass the - * ingest queue (expiration sweeps, NIP-86 admin purges) don't bump it, - * which is why cache entries also carry a short TTL: a snapshot is a - * point-in-time set by NIP-77's nature, and a few seconds of staleness - * only means a peer momentarily re-offers ids the relay just dropped. + * The last few sealed negentropy snapshots, most recently used first, each valid until a write + * could change its set. Replaced whole on every change, never mutated, so a reader never sees a + * half-updated list. Deletion paths that bypass the ingest queue (expiration sweeps, NIP-86 admin + * purges) are not seen here, which is why entries also carry a short TTL: a snapshot is a + * point-in-time set by NIP-77's nature, and a few seconds of staleness only means a peer + * momentarily re-offers ids the relay just dropped. */ - private val writeGeneration = AtomicLong(0L) + private val snapshotCache = AtomicReference<List<CachedSnapshot>>(emptyList()) private class CachedSnapshot( val filterKey: String, - val generation: Long, + val filters: List<Filter>, val builtAt: Long, val storage: IStorage?, ) - private val snapshotCache = AtomicReference<CachedSnapshot?>(null) - /** - * Serves repeated NEG-OPENs of the same filter from one sealed - * storage as long as no write landed in between (single slot — the - * mirror-heartbeat pattern is many peers reconciling the same broad - * filter, not many filters). Rebuilding on every open costs a full - * scan + O(n log n) seal that grows with the corpus: relayBench - * measured 342 ms per identical-set reconcile at 50k events vs - * strfry's 26 ms off its always-current tree. + * Serves repeated NEG-OPENs of the same filter from one sealed storage until a write lands in its + * set. Several slots, because NIP-FE's stateless rounds re-open per round and a relay syncs more + * than one filter at a time; rebuilding costs a full scan + O(n log n) seal that grows with the + * corpus: relayBench measured 342 ms per identical-set reconcile at 50k events vs strfry's 26 ms + * off its always-current tree. */ override suspend fun sealedNegentropyStorage( filters: List<Filter>, @@ -428,30 +427,57 @@ class LiveEventStore( store.liveNegentropySnapshot(maxEntries)?.let { return it } } - val generation = writeGeneration.load() - val key = filters.joinToString("�") { it.toJson() } + "�cap=$maxEntries" + val key = filters.joinToString(" ") { it.toJson() } + " cap=$maxEntries" val now = TimeUtils.now() - val cached = snapshotCache.load() - if (cached != null && - cached.filterKey == key && - cached.generation == generation && - now - cached.builtAt <= SNAPSHOT_TTL_SECONDS - ) { - return cached.storage + snapshotCache.load().firstOrNull { it.filterKey == key && now - it.builtAt <= SNAPSHOT_TTL_SECONDS }?.let { hit -> + updateSnapshots { cached -> listOf(hit) + cached.filter { it !== hit } } + return hit.storage } val built = super.sealedNegentropyStorage(filters, maxEntries) - snapshotCache.store(CachedSnapshot(key, generation, now, built)) + val entry = CachedSnapshot(key, filters, now, built) + updateSnapshots { cached -> (listOf(entry) + cached.filter { it.filterKey != key }).take(SNAPSHOT_SLOTS) } return built } + /** + * Drops every cached snapshot whose set [event] can change: one of its filters admits the event, + * or the event can remove members it cannot see — a deletion or vanish request clears them all, + * and a replaceable or addressable event drops any snapshot whose kinds and authors it falls in, + * since the version it replaces may be in the set under tags or ids the new one no longer has. + */ + private fun forgetSnapshotsCovering(event: Event) { + if (snapshotCache.load().isEmpty()) return + if (event.kind == DeletionRequestEvent.KIND || event.kind == RequestToVanishEvent.KIND) { + updateSnapshots { emptyList() } + return + } + val supersedes = event.kind.isReplaceable() || event.kind.isAddressable() + updateSnapshots { cached -> + cached.filter { snapshot -> + snapshot.filters.none { f -> f.match(event) || (supersedes && Filter(kinds = f.kinds, authors = f.authors).match(event)) } + } + } + } + + private inline fun updateSnapshots(change: (List<CachedSnapshot>) -> List<CachedSnapshot>) { + while (true) { + val current = snapshotCache.load() + val next = change(current) + if (next == current || snapshotCache.compareAndSet(current, next)) return + } + } + private companion object { /** * Ceiling on how long a cached snapshot may serve NEG-OPENs even * with no observed writes — bounds staleness from delete paths - * the generation counter can't see. + * the ingest queue never sees. */ const val SNAPSHOT_TTL_SECONDS = 30L + + /** Distinct filters kept sealed at once; each holds up to maxSyncEvents ids at ~40 B each. */ + const val SNAPSHOT_SLOTS = 4 } } diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/LiveEventStoreSnapshotCacheTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/LiveEventStoreSnapshotCacheTest.kt new file mode 100644 index 0000000000..eacc390d3f --- /dev/null +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/LiveEventStoreSnapshotCacheTest.kt @@ -0,0 +1,133 @@ +/* + * 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.quartz.nip01Core.relay.server.backend + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.store.IEventStore +import com.vitorpamplona.quartz.nip01Core.store.IdAndTime +import com.vitorpamplona.quartz.nip01Core.store.sqlite.DefaultIndexingStrategy +import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventStore +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.runBlocking +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals + +/** The NIP-77 snapshot cache: kept across writes that cannot change its set, dropped by those that can. */ +class LiveEventStoreSnapshotCacheTest { + /** Counts how often a snapshot is actually scanned out of the store. */ + private class Counting( + private val inner: IEventStore, + ) : IEventStore by inner { + var scans = 0 + + override suspend fun snapshotIdsForNegentropy( + filters: List<Filter>, + maxEntries: Int?, + onProgress: ((collected: Int) -> Unit)?, + ): List<IdAndTime> { + scans++ + return inner.snapshotIdsForNegentropy(filters, maxEntries, onProgress) + } + } + + private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + private val store = Counting(EventStore(dbName = null, indexStrategy = DefaultIndexingStrategy(indexEventsByPubkeyAlone = true))) + private val live = LiveEventStore(store, IngestQueue(store, scope.coroutineContext)) + private val alice = NostrSignerSync() + private var clock = 1_700_000_000L + + private val notes = listOf(Filter(kinds = listOf(1))) + private val reactions = listOf(Filter(kinds = listOf(7))) + + @AfterTest + fun tearDown() { + scope.cancel() + } + + private fun event( + kind: Int, + tags: Array<Array<String>> = emptyArray(), + ) = alice.sign<Event>(clock++, kind, tags, "") + + private fun publish(event: Event) = + runBlocking { + val done = CompletableDeferred<IEventStore.InsertOutcome>() + live.submit(event) { done.complete(it) } + assertEquals(IEventStore.InsertOutcome.Accepted, done.await()) + } + + private fun open(filters: List<Filter>) = runBlocking { live.sealedNegentropyStorage(filters, maxEntries = 1_000) } + + @Test + fun twoFiltersInTurnAreEachScannedOnce() { + publish(event(1)) + repeat(3) { + open(notes) + open(reactions) + } + assertEquals(2, store.scans) + } + + @Test + fun aWriteOutsideTheSetKeepsTheSnapshot() { + open(notes) + publish(event(7)) + open(notes) + assertEquals(1, store.scans) + } + + @Test + fun aWriteInsideTheSetRebuildsIt() { + open(notes) + publish(event(1)) + open(notes) + assertEquals(2, store.scans) + } + + @Test + fun aDeletionDropsEverySnapshot() { + open(notes) + open(reactions) + publish(event(5, arrayOf(arrayOf("e", "a".repeat(64))))) + open(notes) + open(reactions) + assertEquals(4, store.scans) + } + + @Test + fun aReplacementDropsASnapshotItsOldVersionWasIn() { + // The stored profile is in the set by its tag; its replacement has no such tag and so does not + // match the filter, but it removes the old version from the set all the same. + val tagged = listOf(Filter(kinds = listOf(0), tags = mapOf("t" to listOf("nostr")))) + publish(event(0, arrayOf(arrayOf("t", "nostr")))) + open(tagged) + publish(event(0)) + open(tagged) + assertEquals(2, store.scans) + } +} From c2ae1578bca94abaad745937f97baeaf42777aa9 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sun, 27 Sep 2026 02:49:26 +0000 Subject: [PATCH 35/60] fix(quartz): NIP-FE review fixes, a policy vote for NIP-98 sign-in, frames without sub id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes from the review of #4212, each with a regression test in HttpRelayHandlerTest: - A deeply nested body threw StackOverflowError out of handle() (the kotlinx tree reader and re-serializer recurse). Bodies are no longer parsed into a tree: JsonShape checks the outer shape (one value, brackets matched by type, depth <= 32, nothing after it), the body is spliced into the frame, and the engine parses it once through RelaySession.receive(String). That also runs IRelayPolicy.acceptMessage again, which receive(Command) skipped, and drops the triple parse (tree, re-serialize, Jackson) to one. - A NIP-98 key was recorded without the policy chain's say, so a FullAuthPolicy whose authorize() refuses a user signed that user in over HTTP. New IRelayPolicy.acceptTransportIdentity(pubkey): the stack records the key only when no member objects, and the default objects. PassThroughPolicy and the verify policies have no objection; FullAuthPolicy asks a new authorizeTransport(pubkey), refusing by default, since authorize() has no AUTH event to run on. RelaySession.authenticateByTransport replaces the initialAuthenticatedUsers parameter #4212 added (and the matching connect/serve overloads), which recorded the key unconditionally. - The size gate compared UTF-8 bytes to a limit the engine counts in chars; it now measures the frame's chars, with a 3x byte bound for the read. - A backend or policy exception escaped handle(); it is now a 500 CLOSED line. - deadline is a Duration: Long.MAX_VALUE ms overflowed into an instant 503. - A reader stalled on a one-frame answer was never dropped; single() now has the same deadline-plus-grace bound as a stream. - An answer frame dequeued at the deadline was replaced by "ran past"; it now goes out, since the answer is complete. - The NIP-98 `u` is read with HTTPAuthorizationEvent.url(), as the verifier reads it; the status table maps MachineReadablePrefix exhaustively. - The handler's verifier is sized for a public endpoint (65,536 live tokens). And the wire format settled upstream in nostr-protocol/nips#2484: answers carry no subscription id — ["EVENT",{…}], ["EOSE"], ["COUNT",{…}], ["CLOSED","<reason>"]. The engine still runs under "http"; the handler strips it by verb, so frames that never carry one pass untouched. /neg stays as this implementation's extension (NIP-FE no longer names it): ["NEG-MSG","<hex>"]. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jDNhr6a3J4VC5TaG3Yd48 --- .../nip01Core/relay/server/RelayServerBase.kt | 21 +- .../nip01Core/relay/server/RelaySession.kt | 28 +- .../relay/server/policies/FullAuthPolicy.kt | 9 + .../relay/server/policies/IRelayPolicy.kt | 15 ++ .../server/policies/PassThroughPolicy.kt | 4 + .../relay/server/policies/PolicyStack.kt | 4 + .../relay/server/policies/VerifyPolicy.kt | 4 + .../nipFERelayOverHttp/HttpRelayCommand.kt | 200 +++++++++------ .../nipFERelayOverHttp/HttpRelayHandler.kt | 240 +++++++++++------- .../nipFERelayOverHttp/HttpRelayStatus.kt | 28 +- .../HttpRelayHandlerTest.kt | 188 ++++++++++++-- 11 files changed, 525 insertions(+), 216 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/RelayServerBase.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/RelayServerBase.kt index f1449f0261..b3fcb9ee5b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/RelayServerBase.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/RelayServerBase.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.quartz.nip01Core.relay.server -import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.server.backend.SessionBackend import com.vitorpamplona.quartz.nip01Core.relay.server.policies.IRelayPolicy import com.vitorpamplona.quartz.nip01Core.relay.server.policies.LimitsPolicy @@ -82,16 +81,8 @@ abstract class RelayServerBase( */ fun connect(send: (String) -> Unit): RelaySession = connect(SessionSink.of(send)) - /** - * Registers a new client connection whose frames go to [sink], typed where - * the engine has the type, already signed in as [authenticatedUsers] when - * the transport proved them itself (a NIP-98 header; see - * [RelaySession.initialAuthenticatedUsers]). - */ - fun connect( - sink: SessionSink, - authenticatedUsers: Set<HexKey> = emptySet(), - ): RelaySession = + /** Registers a new client connection whose frames go to [sink], typed where the engine has the type. */ + fun connect(sink: SessionSink): RelaySession = connections.register( RelaySession( policy = buildPolicy(), @@ -100,7 +91,6 @@ abstract class RelayServerBase( sink = sink, onClose = { connections.unregister(it.id) }, negentropySettings = negentropySettings, - initialAuthenticatedUsers = authenticatedUsers, ), ) @@ -115,15 +105,14 @@ abstract class RelayServerBase( suspend fun serve( send: (String) -> Unit, incoming: suspend (RelaySession) -> Unit, - ) = serve(SessionSink.of(send), emptySet(), incoming) + ) = serve(SessionSink.of(send), incoming) - /** [serve] over a [SessionSink], signed in as [authenticatedUsers] from the start. */ + /** [serve] over a [SessionSink]. */ suspend fun serve( sink: SessionSink, - authenticatedUsers: Set<HexKey>, incoming: suspend (RelaySession) -> Unit, ) { - val session = connect(sink, authenticatedUsers) + val session = connect(sink) try { incoming(session) } finally { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/RelaySession.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/RelaySession.kt index 68793aa486..df77566329 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/RelaySession.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/RelaySession.kt @@ -79,14 +79,6 @@ class RelaySession( * open/close of the same connection. Defaults to a fresh monotonic id. */ val id: Long = nextConnectionId(), - /** - * Identities the transport proved before the session existed — a - * NIP-98 header on an HTTP command (NIP-FE), say — recorded exactly as - * a NIP-42 AUTH would record them. The policy's `accept(AuthCmd)` and - * `onAuthenticated` are not consulted: there is no AUTH event, and the - * transport, not the engine, did the verifying. - */ - initialAuthenticatedUsers: Set<HexKey> = emptySet(), ) : AutoCloseable { /** The original, string-only constructor; every frame goes to [onSend] as wire JSON. */ constructor( @@ -116,7 +108,7 @@ class RelaySession( * the copy costs nothing on the hot path. */ @Volatile - private var authenticatedUsers: Set<HexKey> = initialAuthenticatedUsers.toSet() + private var authenticatedUsers = setOf<HexKey>() /** * The per-connection scope. Handed to the [policy] at connect (so gating @@ -290,6 +282,24 @@ class RelaySession( send(CountMessage(cmd.queryId, countResult)) } + /** + * Records [pubkey], proved by the transport rather than a NIP-42 AUTH (NIP-FE's NIP-98 header), + * once the policy chain has no objection ([IRelayPolicy.acceptTransportIdentity]). Returns the + * refusal, or null when the key is now authenticated on this connection exactly as AUTH would. + */ + suspend fun authenticateByTransport(pubkey: HexKey): String? { + val refused = + try { + policy.acceptTransportIdentity(pubkey) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + MachineReadablePrefix.ERROR.format(e.message ?: "authentication failed") + } + if (refused == null) authenticatedUsers = authenticatedUsers + pubkey + return refused + } + // -- NIP-42: AUTH --------------------------------------------------------- private suspend fun handleAuth(cmd: AuthCmd) { val result = policy.accept(cmd) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/FullAuthPolicy.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/FullAuthPolicy.kt index e13a6cddc8..814083271d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/FullAuthPolicy.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/FullAuthPolicy.kt @@ -129,6 +129,15 @@ open class FullAuthPolicy( */ open suspend fun authorize(event: RelayAuthEvent) {} + final override suspend fun acceptTransportIdentity(pubkey: HexKey): String? = authorizeTransport(pubkey) + + /** + * [authorize]'s counterpart for a key the transport proved (NIP-98 on a NIP-FE command), which + * has no AUTH event to hand it. Refuses by default: a subclass whose [authorize] checks or grants + * anything must decide what that means without one, and opt in by returning null. + */ + open suspend fun authorizeTransport(pubkey: HexKey): String? = TRANSPORT_IDENTITY_REFUSED + override fun accept(cmd: EventCmd): PolicyResult<EventCmd> = if (isAuthenticated()) { PolicyResult.Accepted(cmd) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/IRelayPolicy.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/IRelayPolicy.kt index 978512fee7..2c3440053b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/IRelayPolicy.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/IRelayPolicy.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.server.policies import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.AuthCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command @@ -101,6 +102,17 @@ interface IRelayPolicy { */ suspend fun onAuthenticated(event: RelayAuthEvent): Boolean = false + /** + * This policy's say on recording [pubkey] as authenticated when the transport, not a NIP-42 + * AUTH, proved it: a NIP-98 header on a NIP-FE HTTP command. Return a NIP-01 reason to refuse, + * or null for no objection; the engine records the key only when no policy in the chain + * objects. There is no AUTH event here, so [accept] (AuthCmd) and [onAuthenticated] never run. + * + * The default objects, so a policy that decides logins in those two hooks is not bypassed by a + * transport it was not written for. Policies with no say over identity return null. + */ + suspend fun acceptTransportIdentity(pubkey: HexKey): String? = TRANSPORT_IDENTITY_REFUSED + /** * Inspects a raw inbound message before it is parsed. Return a reason * string to reject it (the engine sends it as a `NOTICE`), or null to let @@ -163,3 +175,6 @@ sealed interface PolicyResult<T : Command> { val reason: String, ) : PolicyResult<T> } + +/** The refusal a policy that does not accept transport-proved identities answers with. */ +const val TRANSPORT_IDENTITY_REFUSED = "restricted: this relay signs in over NIP-42 AUTH only" diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/PassThroughPolicy.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/PassThroughPolicy.kt index 1d673f6e8b..109bd57908 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/PassThroughPolicy.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/PassThroughPolicy.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.server.policies import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.AuthCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CountCmd @@ -52,4 +53,7 @@ open class PassThroughPolicy : IRelayPolicy { override fun accept(cmd: AuthCmd): PolicyResult<AuthCmd> = PolicyResult.Accepted(cmd) override fun canSendToSession(event: Event): Boolean = true + + /** No objection. A subclass that refuses logins in [accept] (AuthCmd) must refuse here too. */ + override suspend fun acceptTransportIdentity(pubkey: HexKey): String? = null } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/PolicyStack.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/PolicyStack.kt index c1062b874a..6adf69c413 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/PolicyStack.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/PolicyStack.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.server.policies import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.AuthCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command @@ -56,6 +57,9 @@ class PolicyStack( return policies.fold(false) { recorded, p -> p.onAuthenticated(event) || recorded } } + /** Every member must agree; the first objection is the answer. */ + override suspend fun acceptTransportIdentity(pubkey: HexKey): String? = policies.firstNotNullOfOrNull { it.acceptTransportIdentity(pubkey) } + override fun acceptMessage(message: String): String? = policies.firstNotNullOfOrNull { it.acceptMessage(message) } override fun acceptSubscription( diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/VerifyPolicy.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/VerifyPolicy.kt index a97c1431fa..9d7286d70b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/VerifyPolicy.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/VerifyPolicy.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.server.policies import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.crypto.verify import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.AuthCmd @@ -68,6 +69,9 @@ open class VerifyEventsAndAuthPolicy( } override fun canSendToSession(event: Event) = true + + /** No objection: this policy checks signatures, and the transport verified its own proof. */ + override suspend fun acceptTransportIdentity(pubkey: HexKey): String? = null } /** diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayCommand.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayCommand.kt index 304735bf06..6d48801955 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayCommand.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayCommand.kt @@ -20,35 +20,27 @@ */ package com.vitorpamplona.quartz.nipFERelayOverHttp -import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ClosedMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EoseMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NoticeMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage -import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CountCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd import com.vitorpamplona.quartz.nip77Negentropy.NegErrMessage import com.vitorpamplona.quartz.nip77Negentropy.NegMsgMessage import com.vitorpamplona.quartz.nip77Negentropy.NegOpenCmd -import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive /** - * NIP-FE: the client commands HTTP carries, one path each. A body is the - * command's arguments after its subscription id (a lone object where the - * command takes one); the answer ends on the first frame [ends] accepts. + * NIP-FE: the client commands HTTP carries, one path each. A body is the command's arguments + * after its subscription id (a lone object where the command takes one); the answer ends on the + * first frame [ends] accepts. * - * `NEG` is one NIP-77 round, `[filter, message]`: the responder keeps no - * state between rounds but its snapshot, which the backend caches per - * filter, so each round carries its filter and there is no session to close. + * `NEG` is not in NIP-FE; it is this implementation's extension: one NIP-77 round, + * `[filter, message]`. The responder keeps no state between rounds but its snapshot, which the + * backend caches per filter, so each round carries its filter and there is no session to close. */ enum class HttpRelayCommand( val path: String, @@ -60,28 +52,35 @@ enum class HttpRelayCommand( ; /** - * The command [body] stands for, parsed and validated, or null when it - * is not this command's arguments. Re-serialized from a JSON tree, so a - * body can only ever be arguments, never a second command. + * The client frame [body] stands for, or null when it is not this command's arguments. Only + * the body's outer shape is checked here ([JsonShape]); the engine parses the frame once, so a + * malformed inside is its usual NOTICE. The shape check is what makes splicing safe: the body + * is one balanced value with nothing after it, so it cannot close the frame or open another. */ - fun parse(body: String): Parsed? { - val tree = - try { - Json.parseToJsonElement(body) - } catch (_: SerializationException) { - return null + fun frameOf(body: String): String? { + val shape = JsonShape.of(body) ?: return null + return when (this) { + REQ, COUNT -> { + val filters = + when { + shape.isObject -> shape.text + shape.elements.isNotEmpty() && shape.elements.all { it == '{' } -> shape.inner + else -> return null + } + frame(if (this == REQ) ReqCmd.LABEL else CountCmd.LABEL, SUB_ID, filters) } - val args = arguments(tree) ?: return null - val frame = JsonArray(head() + args).toString() - val cmd = runCatching { OptimizedJsonMapper.fromJsonToCommand(frame) }.getOrNull() ?: return null - return if (cmd.isValid() && cmd.matches()) Parsed(cmd, frame.length) else null - } - /** A body as its command, with the length of the frame it stands for: what the message-length limit measures. */ - class Parsed( - val command: Command, - val wireLength: Int, - ) + EVENT -> { + if (!shape.isObject) return null + "[\"${EventCmd.LABEL}\",${shape.text}]" + } + + NEG -> { + if (shape.isObject || shape.elements != NEG_ROUND) return null + frame(NegOpenCmd.LABEL, SUB_ID, shape.inner) + } + } + } /** Whether [message] is the last frame of this command's answer. */ fun ends(message: Message): Boolean = @@ -93,47 +92,106 @@ enum class HttpRelayCommand( NEG -> message is NegMsgMessage || message is NegErrMessage } - private fun head(): List<JsonElement> = - when (this) { - REQ -> listOf(JsonPrimitive(ReqCmd.LABEL), JsonPrimitive(SUB_ID)) - COUNT -> listOf(JsonPrimitive(CountCmd.LABEL), JsonPrimitive(SUB_ID)) - EVENT -> listOf(JsonPrimitive(EventCmd.LABEL)) - NEG -> listOf(JsonPrimitive(NegOpenCmd.LABEL), JsonPrimitive(SUB_ID)) - } - - private fun arguments(body: JsonElement): List<JsonElement>? = - when (this) { - REQ, COUNT -> { - when (body) { - is JsonObject -> listOf(body) - is JsonArray -> body.takeIf { it.isNotEmpty() && it.all { f -> f is JsonObject } } - else -> null - } - } - - EVENT -> { - (body as? JsonObject)?.let(::listOf) - } - - NEG -> { - (body as? JsonArray)?.takeIf { - it.size == 2 && it[0] is JsonObject && (it[1] as? JsonPrimitive)?.isString == true - } - } - } - - private fun Command.matches(): Boolean = - when (this@HttpRelayCommand) { - REQ -> this is ReqCmd - COUNT -> this is CountCmd - EVENT -> this is EventCmd - NEG -> this is NegOpenCmd - } - companion object { - /** The subscription id every HTTP command runs under; each request is its own connection. */ + /** + * The subscription id every HTTP command runs under inside the engine. NIP-FE answers carry + * none, so [HttpRelayHandler] takes it back out of each frame before it goes out. + */ const val SUB_ID = "http" + private val NEG_ROUND = listOf('{', '"') + fun forPath(path: String): HttpRelayCommand? = entries.firstOrNull { it.path == path } + + private fun frame( + label: String, + subId: String, + args: String, + ) = "[\"$label\",\"$subId\",$args]" + } +} + +/** + * The outer shape of a JSON body, read without building a tree: one object or array, brackets + * matched by type outside strings, nesting no deeper than [MAX_DEPTH], nothing after it. An array's + * [elements] are each top-level element's first character. Everything inside is left to the parser. + */ +internal class JsonShape private constructor( + val text: String, + val isObject: Boolean, + val elements: List<Char>, +) { + /** An array's contents without its brackets. */ + val inner: String get() = text.substring(1, text.length - 1) + + companion object { + /** Deep enough for any filter, event or round by a wide margin; far too shallow to exhaust a stack. */ + const val MAX_DEPTH = 32 + + fun of(body: String): JsonShape? { + val text = body.trim() + if (text.length < 2 || (text[0] != '{' && text[0] != '[')) return null + val elements = ArrayList<Char>() + val open = CharArray(MAX_DEPTH) + var depth = 0 + var inString = false + var escaped = false + // At depth 1 inside an array: whether the next non-space character starts an element. + var expectElement = text[0] == '[' + var i = 0 + while (i < text.length) { + val c = text[i] + if (inString) { + when { + escaped -> escaped = false + c == '\\' -> escaped = true + c == '"' -> inString = false + } + i++ + continue + } + if (depth == 0 && i > 0) return null + if (depth == 1 && text[0] == '[' && !c.isWhitespace()) { + when { + c == ',' -> { + if (expectElement) return null + expectElement = true + i++ + continue + } + + c == ']' -> { + if (expectElement && elements.isNotEmpty()) return null + } + + expectElement -> { + elements.add(c) + expectElement = false + } + + c == '{' || c == '[' || c == '"' -> { + return null + } + } + } + when (c) { + '"' -> { + inString = true + } + + '{', '[' -> { + if (depth == MAX_DEPTH) return null + open[depth++] = c + } + + '}', ']' -> { + if (depth == 0 || open[--depth] != (if (c == '}') '{' else '[')) return null + } + } + i++ + } + if (depth != 0 || inString) return null + return JsonShape(text, text[0] == '{', elements) + } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt index e28f86815d..e443028888 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt @@ -28,7 +28,9 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.MachineReadabl import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.nip01Core.relay.server.RelayServerBase import com.vitorpamplona.quartz.nip01Core.relay.server.SessionSink +import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent import com.vitorpamplona.quartz.nip98HttpAuth.Nip98AuthVerifier +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.channels.Channel @@ -38,20 +40,27 @@ import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeoutOrNull import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.TimeSource -/** One NIP-FE request as the handler needs it. The host routes by [HttpRelayCommand.path] and bounds the body while reading it. */ +/** One NIP-FE request as the handler needs it. The host routes by [HttpRelayCommand.path]. */ class HttpRelayRequest( val command: HttpRelayCommand, /** The `Authorization` header as sent, or null. */ val authorization: String?, + /** + * The body. Hosts bound the read at [HttpRelayHandler.maxBodyBytes]: the engine measures a + * frame in characters, and a UTF-8 character takes up to three bytes. + */ val body: ByteArray, ) /** - * Where [HttpRelayHandler] writes an answer. The host owns the socket, the - * headers and any compression; the handler decides the status and the lines. - * On 401 the host adds `WWW-Authenticate: Nostr`, on 429 and 503 `Retry-After`. + * Where [HttpRelayHandler] writes an answer. The host owns the socket, the headers and any + * compression; the handler decides the status and the lines. On 401 the host adds + * `WWW-Authenticate: Nostr`, on 429 and 503 `Retry-After`. A [HttpRelayReaderStalled] thrown out of + * either call means the client stopped reading, and the host drops the connection unfinished. */ interface HttpRelayResponse { /** An answer that is one frame, with its status: a refusal, or a command answered at once. */ @@ -60,11 +69,7 @@ interface HttpRelayResponse { frame: String, ) - /** - * A 200 answer written line by line, as `application/x-ndjson`. Returns - * when [lines] does; a [HttpRelayReaderStalled] thrown out of it means - * the client stopped reading and the host drops the connection unfinished. - */ + /** A 200 answer written line by line, as `application/x-ndjson`. Returns when [lines] does. */ suspend fun stream(lines: suspend HttpRelayLines.() -> Unit) } @@ -77,129 +82,148 @@ interface HttpRelayLines { suspend fun flush() } -/** The client stopped reading a streamed answer; the host drops the connection instead of finishing it. */ +/** The client stopped reading an answer; the host drops the connection instead of finishing it. */ class HttpRelayReaderStalled : Exception("the client stopped reading the answer") /** - * NIP-FE: one relay command per HTTP request, run on its own [RelayServerBase] - * session, so every limit and policy the socket applies applies here, and - * answered with the relay's own frames up to the command's answer. Nothing - * outlives the request. Admission (how many requests a client may run) is the - * host's: gate before calling [handle], so a refused request does not spend a - * NIP-98 token that the handler would have verified. + * NIP-FE: one relay command per HTTP request, run on its own [RelayServerBase] session, so every + * limit and policy the socket applies applies here, and answered with the relay's own frames up to + * the command's answer, without their subscription id. Nothing outlives the request. + * + * Admission (how many requests a client may run) is the host's: gate before calling [handle], so a + * refused request does not spend a NIP-98 token the handler would have verified. */ class HttpRelayHandler( private val server: RelayServerBase, /** The prefixes a NIP-98 `u` may carry (the relay's http origin, its .onion), asked per request; never from the request. */ private val origins: () -> List<String>, - /** How long one answer may run, first byte to last. */ - private val deadlineMs: Long = DEFAULT_DEADLINE_MS, - /** Its own replay cache, so public commands cannot evict another endpoint's. */ - private val verifier: Nip98AuthVerifier = Nip98AuthVerifier(), + /** How long one answer may run, first byte to last. [Duration.INFINITE] turns the deadline off. */ + private val deadline: Duration = DEFAULT_DEADLINE, + /** Its own replay cache, sized for a public endpoint, so it cannot be flushed to replay a token. */ + private val verifier: Nip98AuthVerifier = Nip98AuthVerifier(maxReplayEntries = DEFAULT_REPLAY_ENTRIES), /** Frames queued ahead of a slow reader before the answer is cut short. */ private val maxQueuedFrames: Int = DEFAULT_MAX_QUEUED_FRAMES, /** How long past the deadline the last line may take before the reader counts as stalled. */ - private val tailGraceMs: Long = DEFAULT_TAIL_GRACE_MS, + private val tailGrace: Duration = DEFAULT_TAIL_GRACE, ) { + /** The largest body that can still be a frame within the relay's message limit, or null for no limit. */ + val maxBodyBytes: Long? get() = server.limits?.maxMessageLength?.let { it.toLong() * 3 } + suspend fun handle( request: HttpRelayRequest, response: HttpRelayResponse, ) { val command = request.command val max = server.limits?.maxMessageLength - if (max != null && request.body.size > max) { - return response.single(HttpRelayStatus.PAYLOAD_TOO_LARGE, closed("invalid: the body exceeds $max bytes")) + maxBodyBytes?.let { cap -> + if (request.body.size > cap) { + return response.single(HttpRelayStatus.PAYLOAD_TOO_LARGE, closed("invalid: the command exceeds $max characters")) + } } - val parsed = - command.parse(request.body.decodeToString()) + val frame = + command.frameOf(request.body.decodeToString()) ?: return response.single(HttpRelayStatus.BAD_REQUEST, closed("invalid: the body is not ${command.name}'s arguments")) - if (max != null && parsed.wireLength > max) { + // Characters, as the engine's own limit counts them. + if (max != null && frame.length > max) { return response.single(HttpRelayStatus.PAYLOAD_TOO_LARGE, closed("invalid: the command exceeds $max characters")) } val signedIn = when (val proof = proofOf(request)) { - is Proof.Anonymous -> emptySet() - is Proof.Signed -> setOf(proof.pubkey) - is Proof.Refused -> return response.single(HttpRelayStatus.UNAUTHORIZED, closed(MachineReadablePrefix.AUTH_REQUIRED.format(proof.reason))) + is Proof.Anonymous -> { + null + } + + is Proof.Signed -> { + proof.pubkey + } + + is Proof.Refused -> { + val reason = proof.reason + return response.single(HttpRelayStatus.forReason(reason), closed(reason)) + } } - exchange(parsed, command, signedIn, response) + exchange(frame, command, signedIn, response) } - /** A frame as queued: its wire text, and its type when the engine built one. */ + /** A frame as queued: its wire text, its type when the engine built one, and whether it ends the answer. */ private class Frame( val json: String, val message: Message?, + val last: Boolean, ) - private fun HttpRelayCommand.ends(frame: Frame) = frame.message?.let(::ends) == true - private suspend fun exchange( - parsed: HttpRelayCommand.Parsed, + frame: String, command: HttpRelayCommand, - signedIn: Set<HexKey>, + signedIn: HexKey?, response: HttpRelayResponse, ) = coroutineScope { val frames = Channel<Frame>(maxQueuedFrames) val ended = CompletableDeferred<Unit>() // Called on the engine's coroutines and cannot suspend, so a frame that does not fit ends the answer. - fun offer( - frame: Frame, - last: Boolean, - ) { + fun offer(frame: Frame) { val sent = frames.trySend(frame) if (sent.isClosed) return - if (sent.isFailure || last) { + if (sent.isFailure || frame.last) { frames.close() ended.complete(Unit) } } + + fun fail(reason: String) = offer(Frame(closed(reason), ClosedMessage(HttpRelayCommand.SUB_ID, reason), last = true)) val sink = object : SessionSink { override fun message(message: Message) { - // The challenge every connection opens with; this one proved its key by NIP-98 instead. + // The challenge every connection opens with; this one proves its key by NIP-98 instead. if (message is AuthMessage) return - offer(Frame(message.toJson(), message), command.ends(message)) + offer(Frame(withoutSubId(message.toJson()), message, command.ends(message))) } - override fun raw(json: String) = offer(Frame(json, null), false) + override fun raw(json: String) = offer(Frame(withoutSubId(json), null, last = false)) } val session = launch { - server.serve(sink, signedIn) { - it.receive(parsed.command) - ended.await() + try { + server.serve(sink) { session -> + val refused = signedIn?.let { session.authenticateByTransport(it) } + if (refused != null) fail(refused) else session.receive(frame) + ended.await() + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + // A backend or policy that throws is the relay's failure, answered as one. + fail(MachineReadablePrefix.ERROR.format(e.message ?: "the relay failed this command")) } } - val started = TimeSource.Monotonic.markNow() + val due = TimeSource.Monotonic.markNow() + deadline - fun remainingMs() = (deadlineMs - started.elapsedNow().inWholeMilliseconds).coerceAtLeast(0) + suspend fun single( + status: Int, + json: String, + ) = bounded(due) { response.single(status, json) } try { - val first = withTimeoutOrNull(deadlineMs) { frames.receiveCatching().getOrNull() } + val first = withTimeoutOrNull(deadline) { frames.receiveCatching().getOrNull() } + val status = HttpRelayStatus.of(first?.message) when { first == null -> { - response.single(HttpRelayStatus.UNAVAILABLE, closed("error: no answer within ${deadlineMs / 1000}s")) + single(HttpRelayStatus.UNAVAILABLE, closed("error: no answer within $deadline")) } - command.ends(first) || HttpRelayStatus.of(first.message) != HttpRelayStatus.OK -> { - response.single(HttpRelayStatus.of(first.message), first.json) + first.last || status != HttpRelayStatus.OK -> { + single(status, first.json) } else -> { response.stream { - // The deadline is read between frames and never interrupts a write, so every line leaves - // whole; a reader that stops reading altogether is dropped at the hard stop. - try { - withTimeout(remainingMs() + tailGraceMs) { - when (drain(first, frames, command, started)) { - Ending.ANSWERED -> {} - Ending.DEADLINE -> line(closed("error: the answer ran past ${deadlineMs / 1000}s")) - Ending.CUT -> line(closed("error: slow reader, over $maxQueuedFrames frames waiting")) - } - flush() + bounded(due) { + when (drain(first, frames, due)) { + Ending.ANSWERED -> {} + Ending.DEADLINE -> line(closed("error: the answer ran past $deadline")) + Ending.CUT -> line(closed("error: slow reader, over $maxQueuedFrames frames waiting")) } - } catch (_: TimeoutCancellationException) { - throw HttpRelayReaderStalled() + flush() } } } @@ -209,32 +233,46 @@ class HttpRelayHandler( } } + /** Runs [block] until [due] plus the tail grace; a write still blocked then is a reader that stopped. */ + private suspend fun bounded( + due: TimeSource.Monotonic.ValueTimeMark, + block: suspend () -> Unit, + ) { + val left = (-due.elapsedNow()).coerceAtLeast(Duration.ZERO) + tailGrace + try { + withTimeout(left) { block() } + } catch (_: TimeoutCancellationException) { + throw HttpRelayReaderStalled() + } + } + /** How a streamed answer stopped: at its answer frame, at the deadline, or cut because the reader fell behind. */ private enum class Ending { ANSWERED, DEADLINE, CUT } /** - * Writes [first] and what follows up to the command's answer, flushing whenever nothing is waiting so - * a burst leaves as one write. Stops at the answer: a live event queued behind it is not part of it. + * Writes [first] and what follows up to the command's answer, flushing whenever nothing is + * waiting so a burst leaves as one write. Stops at the answer: a live event queued behind it is + * not part of it. The deadline is read between frames and never interrupts a write, so every line + * leaves whole, and the answer frame goes out even at the deadline: the answer is complete. */ private suspend fun HttpRelayLines.drain( first: Frame, frames: Channel<Frame>, - command: HttpRelayCommand, - started: TimeSource.Monotonic.ValueTimeMark, + due: TimeSource.Monotonic.ValueTimeMark, ): Ending { var frame = first while (true) { line(frame.json) - if (command.ends(frame)) return Ending.ANSWERED + if (frame.last) return Ending.ANSWERED frame = frames.tryReceive().getOrNull() ?: run { flush() - val leftMs = deadlineMs - started.elapsedNow().inWholeMilliseconds - if (leftMs <= 0) return Ending.DEADLINE - val next = withTimeoutOrNull(leftMs) { frames.receiveCatching() } ?: return Ending.DEADLINE + val left = -due.elapsedNow() + if (!left.isPositive()) return Ending.DEADLINE + val next = withTimeoutOrNull(left) { frames.receiveCatching() } ?: return Ending.DEADLINE // Closed with no answer frame in it: the send side gave up on this reader. next.getOrNull() ?: return Ending.CUT } - if (started.elapsedNow().inWholeMilliseconds >= deadlineMs) return Ending.DEADLINE + if (!frame.last && due.hasPassedNow()) return Ending.DEADLINE } } @@ -262,34 +300,64 @@ class HttpRelayHandler( if (!header.regionMatches(0, scheme, 0, scheme.length, ignoreCase = true)) return Proof.Anonymous val token = scheme + header.substring(scheme.length).trim() val accepted = origins().map { it.trimEnd('/') + request.command.path } - val url = claimedUrl(token)?.takeIf { it in accepted } ?: accepted.firstOrNull() ?: return Proof.Refused("this relay names no url to sign") + val url = claimedUrl(token)?.takeIf { it in accepted } ?: accepted.firstOrNull() ?: return Proof.Refused(MachineReadablePrefix.AUTH_REQUIRED.format("this relay names no url to sign")) return when (val r = verifier.verify(token, "POST", url, request.body)) { - is Nip98AuthVerifier.Result.Verified -> Proof.Signed(r.pubkey) - is Nip98AuthVerifier.Result.Malformed -> Proof.Refused("NIP-98 ${r.reason}") - is Nip98AuthVerifier.Result.Missing -> Proof.Anonymous + is Nip98AuthVerifier.Result.Verified -> { + Proof.Signed(r.pubkey) + } + + is Nip98AuthVerifier.Result.Missing -> { + Proof.Anonymous + } + + // A full replay cache is the relay's limit, not the token's fault. + is Nip98AuthVerifier.Result.Malformed -> { + if (MachineReadablePrefix.parse(r.reason) == MachineReadablePrefix.RATE_LIMITED) { + Proof.Refused(r.reason) + } else { + Proof.Refused(MachineReadablePrefix.AUTH_REQUIRED.format("NIP-98 ${r.reason}")) + } + } } } - /** The `u` tag of a NIP-98 token, or null when it does not decode; the verifier then says why. */ + /** The `u` a NIP-98 token names, read as the verifier reads it, or null when it does not decode. */ @OptIn(ExperimentalEncodingApi::class) private fun claimedUrl(token: String): String? = runCatching { val json = Base64.decode(token.removePrefix(Nip98AuthVerifier.SCHEME).trim()).decodeToString() - OptimizedJsonMapper - .fromJson(json) - .tags - .firstOrNull { it.size > 1 && it[0] == "u" } - ?.get(1) + val event = OptimizedJsonMapper.fromJson(json) + HTTPAuthorizationEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig).url() }.getOrNull() - private fun closed(reason: String) = ClosedMessage(HttpRelayCommand.SUB_ID, reason).toJson() + private fun closed(reason: String) = withoutSubId(ClosedMessage(HttpRelayCommand.SUB_ID, reason).toJson()) companion object { - const val DEFAULT_DEADLINE_MS = 30_000L + val DEFAULT_DEADLINE = 30_000.milliseconds /** The websocket's slow-consumer bound in the reference relays. */ const val DEFAULT_MAX_QUEUED_FRAMES = 8192 - const val DEFAULT_TAIL_GRACE_MS = 5_000L + val DEFAULT_TAIL_GRACE = 5_000.milliseconds + + /** Two minutes of tokens (the replay window) at about 500 signed commands a second. */ + const val DEFAULT_REPLAY_ENTRIES = 65_536 } } + +/** The frames that carry a subscription id in the engine; NIP-FE sends them without it. */ +private val SUBSCRIPTION_FRAMES = setOf("EVENT", "EOSE", "CLOSED", "COUNT", "NEG-MSG", "NEG-ERR") + +private const val SUB_ID_FIELD = ",\"" + HttpRelayCommand.SUB_ID + "\"" + +/** + * [frame] as NIP-FE sends it: the engine's frame with its `"http"` subscription id taken out, + * `["EVENT","http",{…}]` → `["EVENT",{…}]`, `["EOSE","http"]` → `["EOSE"]`. Other frames pass as they are. + */ +internal fun withoutSubId(frame: String): String { + if (!frame.startsWith("[\"")) return frame + val verbEnd = frame.indexOf('"', 2) + if (verbEnd < 0 || frame.substring(2, verbEnd) !in SUBSCRIPTION_FRAMES) return frame + if (!frame.startsWith(SUB_ID_FIELD, verbEnd + 1)) return frame + return frame.substring(0, verbEnd + 1) + frame.substring(verbEnd + 1 + SUB_ID_FIELD.length) +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayStatus.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayStatus.kt index 41f8fb64a2..496acab463 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayStatus.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayStatus.kt @@ -21,16 +21,17 @@ package com.vitorpamplona.quartz.nipFERelayOverHttp import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ClosedMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.MachineReadablePrefix import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NoticeMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage +import com.vitorpamplona.quartz.nip01Core.store.RejectionReason import com.vitorpamplona.quartz.nip77Negentropy.NegErrMessage /** - * NIP-FE status codes. The status of an answer is decided by its first - * frame: an accepting one is 200 and the answer may stream; a refusal's - * NIP-01 machine-readable prefix picks the code. After the first frame is - * out the status cannot change, so later failures are frames. + * NIP-FE status codes. The status of an answer is decided by its first frame: an accepting one + * is 200 and the answer may stream; a refusal's NIP-01 machine-readable prefix picks the code. + * After the first frame is out the status cannot change, so later failures are frames. */ object HttpRelayStatus { const val OK = 200 @@ -42,24 +43,25 @@ object HttpRelayStatus { const val INTERNAL_ERROR = 500 const val UNAVAILABLE = 503 - /** The status an answer opening with [message] carries. A raw EVENT frame opens a 200. */ + /** The status an answer opening with [message] carries. A raw EVENT frame (no type) opens a 200. */ fun of(message: Message?): Int = when (message) { is ClosedMessage -> forReason(message.message) is NegErrMessage -> forReason(message.reason) // A duplicate is already stored, which is what the caller asked for, whichever flag the store set. - is OkMessage -> if (message.success || message.message.startsWith("duplicate:")) OK else forReason(message.message) + is OkMessage -> if (message.success || message.message.startsWith(RejectionReason.PREFIX_DUPLICATE)) OK else forReason(message.message) is NoticeMessage -> BAD_REQUEST else -> OK } - /** The status a NIP-01 machine-readable prefix stands for. */ + /** The status a NIP-01 machine-readable prefix stands for; a reason without one is a 400. */ fun forReason(reason: String): Int = - when (reason.substringBefore(':', "")) { - "auth-required" -> UNAUTHORIZED - "restricted", "blocked" -> FORBIDDEN - "rate-limited" -> TOO_MANY_REQUESTS - "error" -> INTERNAL_ERROR - else -> BAD_REQUEST + when (MachineReadablePrefix.parse(reason)) { + MachineReadablePrefix.AUTH_REQUIRED -> UNAUTHORIZED + MachineReadablePrefix.RESTRICTED, MachineReadablePrefix.BLOCKED -> FORBIDDEN + MachineReadablePrefix.RATE_LIMITED -> TOO_MANY_REQUESTS + MachineReadablePrefix.ERROR -> INTERNAL_ERROR + MachineReadablePrefix.DUPLICATE -> OK + MachineReadablePrefix.INVALID, MachineReadablePrefix.POW, MachineReadablePrefix.UNSUPPORTED, null -> BAD_REQUEST } } diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt index e6837eb856..4123405322 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt @@ -23,15 +23,20 @@ package com.vitorpamplona.quartz.nipFERelayOverHttp import com.vitorpamplona.negentropy.Negentropy import com.vitorpamplona.negentropy.storage.StorageVector import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.MachineReadablePrefix import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.server.RelayServerBase import com.vitorpamplona.quartz.nip01Core.relay.server.RelayServerListener import com.vitorpamplona.quartz.nip01Core.relay.server.backend.RequestContext import com.vitorpamplona.quartz.nip01Core.relay.server.backend.SessionBackend +import com.vitorpamplona.quartz.nip01Core.relay.server.policies.FullAuthPolicy +import com.vitorpamplona.quartz.nip01Core.relay.server.policies.IRelayPolicy +import com.vitorpamplona.quartz.nip01Core.relay.server.policies.LimitsPolicy import com.vitorpamplona.quartz.nip01Core.relay.server.policies.PassThroughPolicy import com.vitorpamplona.quartz.nip01Core.relay.server.policies.PolicyResult import com.vitorpamplona.quartz.nip01Core.relay.server.policies.RelayLimits @@ -39,8 +44,10 @@ import com.vitorpamplona.quartz.nip01Core.relay.server.policies.VerifyPolicy import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.store.IEventStore import com.vitorpamplona.quartz.nip01Core.store.IdAndTime +import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip77Negentropy.NegentropySettings import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent +import com.vitorpamplona.quartz.nip98HttpAuth.Nip98AuthVerifier import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.runBlocking @@ -49,6 +56,9 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertTrue +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds /** NIP-FE's handler over a relay engine and an in-memory backend: status, lines, auth, and negentropy rounds. */ class HttpRelayHandlerTest { @@ -108,15 +118,19 @@ class HttpRelayHandlerTest { } private class MemoryRelay( - override val backend: MemoryBackend, - signedInOnly: Boolean, + override val backend: SessionBackend, + policies: () -> IRelayPolicy, + limits: RelayLimits? = RelayLimits(maxMessageLength = 4096), ) : RelayServerBase( - policyBuilder = { if (signedInOnly) VerifyPolicy + SignedInOnly() else VerifyPolicy }, + policyBuilder = policies, parentContext = SupervisorJob(), negentropySettings = NegentropySettings.Default, listener = RelayServerListener.None, - limits = RelayLimits(maxMessageLength = 4096), - ) + limits = limits, + ) { + constructor(backend: SessionBackend, signedInOnly: Boolean) : + this(backend, { if (signedInOnly) VerifyPolicy + SignedInOnly() else VerifyPolicy }) + } /** One answer as the host would see it. */ private class Recorded : HttpRelayResponse { @@ -152,8 +166,8 @@ class HttpRelayHandlerTest { private fun handler( signedInOnly: Boolean = false, - deadlineMs: Long = 5_000, - ) = HttpRelayHandler(MemoryRelay(backend, signedInOnly), origins = { listOf(origin) }, deadlineMs = deadlineMs) + deadline: Duration = 5.seconds, + ) = HttpRelayHandler(MemoryRelay(backend, signedInOnly), origins = { listOf(origin) }, deadline = deadline) private fun HttpRelayHandler.ask( command: HttpRelayCommand, @@ -179,7 +193,7 @@ class HttpRelayHandlerTest { val answer = handler().ask(HttpRelayCommand.REQ, """{"kinds":[1]}""") assertEquals(200, answer.status) assertTrue(answer.streamed) - assertEquals("""["EOSE","http"]""", answer.lines.last()) + assertEquals("""["EOSE"]""", answer.lines.last()) assertEquals( setOf(a.id, b.id), answer.lines @@ -193,7 +207,7 @@ class HttpRelayHandlerTest { fun anEmptyReqIsOneEoseLine() { val answer = handler().ask(HttpRelayCommand.REQ, """[{"kinds":[30000]}]""") assertEquals(200, answer.status) - assertEquals(listOf("""["EOSE","http"]"""), answer.lines) + assertEquals(listOf("""["EOSE"]"""), answer.lines) } @Test @@ -215,22 +229,26 @@ class HttpRelayHandlerTest { backend.events += listOf(note("a"), note("b"), note("c")) val answer = handler().ask(HttpRelayCommand.COUNT, """{"kinds":[1]}""") assertEquals(200, answer.status) - assertTrue(answer.lines.single().startsWith("""["COUNT","http",{"count":3"""), answer.lines.toString()) + assertTrue(answer.lines.single().startsWith("""["COUNT",{"count":3"""), answer.lines.toString()) } @Test fun aBodyThatIsNotTheCommandsArgumentsIsA400AndOneOverTheLimitA413() { + // Not the command's shape: refused before any session opens. for ((command, body) in listOf( HttpRelayCommand.REQ to "[]", HttpRelayCommand.EVENT to """[{"id":"x"}]""", - HttpRelayCommand.EVENT to """{"id":"not an event"}""", HttpRelayCommand.NEG to """[{"kinds":[1]}]""", HttpRelayCommand.COUNT to "not json", )) { val answer = handler().ask(command, body) assertEquals(400, answer.status, "$command '$body'") - assertTrue(answer.lines.single().startsWith("""["CLOSED","http","invalid:"""), answer.lines.toString()) + assertTrue(answer.lines.single().startsWith("""["CLOSED","invalid:"""), answer.lines.toString()) } + // The right shape with an inside the engine cannot read: its own NOTICE, the command never ran. + val unreadable = handler().ask(HttpRelayCommand.EVENT, """{"id":"not an event"}""") + assertEquals(400, unreadable.status) + assertTrue(unreadable.lines.single().startsWith("""["NOTICE","""), unreadable.lines.toString()) assertEquals(413, handler().ask(HttpRelayCommand.REQ, """{"search":"${"x".repeat(5_000)}"}""").status) // Under the byte cap, over it once wrapped in its frame: the engine measures the frame. assertEquals(413, handler().ask(HttpRelayCommand.REQ, """{"search":"${"x".repeat(4_096 - 20)}"}""").status) @@ -244,13 +262,13 @@ class HttpRelayHandlerTest { val anonymous = gated.ask(HttpRelayCommand.REQ, body) assertEquals(401, anonymous.status) - assertTrue(anonymous.lines.single().startsWith("""["CLOSED","http","auth-required:""")) + assertTrue(anonymous.lines.single().startsWith("""["CLOSED","auth-required:""")) assertEquals(401, gated.ask(HttpRelayCommand.REQ, body, "Basic dXNlcjpwYXNz").status, "Basic is not addressed to the relay") val signed = gated.ask(HttpRelayCommand.REQ, body, token(HttpRelayCommand.REQ, body)) assertEquals(200, signed.status, signed.lines.toString()) - assertEquals("""["EOSE","http"]""", signed.lines.last()) + assertEquals("""["EOSE"]""", signed.lines.last()) } @Test @@ -288,7 +306,7 @@ class HttpRelayHandlerTest { val reply = answer.lines .single() - .substringAfter("""["NEG-MSG","http","""") + .substringAfter("""["NEG-MSG","""") .substringBefore('"') val result = client.reconcile(reply.hexToByteArray()) have += result.sendIds.map { it.toHexString() } @@ -303,30 +321,30 @@ class HttpRelayHandlerTest { fun aMalformedNegentropyRoundIsRefused() { val answer = handler().ask(HttpRelayCommand.NEG, """[{"kinds":[1]},"zz"]""") assertEquals(400, answer.status) - assertTrue(answer.lines.single().startsWith("""["NEG-ERR","http","""), answer.lines.toString()) + assertTrue(answer.lines.single().startsWith("""["NEG-ERR","""), answer.lines.toString()) } @Test fun noFirstFrameWithinTheDeadlineIsA503() { - val answer = handler(deadlineMs = 300).ask(HttpRelayCommand.REQ, """{"kinds":[$STALLED_KIND]}""") + val answer = handler(deadline = 300.milliseconds).ask(HttpRelayCommand.REQ, """{"kinds":[$STALLED_KIND]}""") assertEquals(503, answer.status) - assertTrue(answer.lines.single().startsWith("""["CLOSED","http","error: no answer""")) + assertTrue(answer.lines.single().startsWith("""["CLOSED","error: no answer""")) } @Test fun aDeadlineMidAnswerEndsOnAClosedLine() { val found = note("found") backend.events += found - val answer = handler(deadlineMs = 300).ask(HttpRelayCommand.REQ, """{"kinds":[1,$TRICKLE_KIND]}""") + val answer = handler(deadline = 300.milliseconds).ask(HttpRelayCommand.REQ, """{"kinds":[1,$TRICKLE_KIND]}""") assertEquals(200, answer.status) assertTrue(found.id in answer.lines.first()) - assertTrue(answer.lines.last().startsWith("""["CLOSED","http","error: the answer ran past"""), answer.lines.toString()) + assertTrue(answer.lines.last().startsWith("""["CLOSED","error: the answer ran past"""), answer.lines.toString()) } @Test fun aReaderThatStopsReadingIsDroppedAtTheHardStop() { backend.events += note("found") - val h = HttpRelayHandler(MemoryRelay(backend, false), origins = { listOf(origin) }, deadlineMs = 100, tailGraceMs = 100) + val h = HttpRelayHandler(MemoryRelay(backend, false), origins = { listOf(origin) }, deadline = 100.milliseconds, tailGrace = 100.milliseconds) val stalled = object : HttpRelayResponse { override suspend fun single( @@ -356,6 +374,134 @@ class HttpRelayHandlerTest { session.close() } + @Test + fun framesCarryNoSubscriptionId() { + val found = note("found") + backend.events += found + val answer = handler().ask(HttpRelayCommand.REQ, """{"kinds":[1]}""") + assertTrue(answer.lines.first().startsWith("""["EVENT",{"""), answer.lines.toString()) + assertEquals("""["EOSE"]""", answer.lines.last()) + } + + @Test + fun aDeeplyNestedBodyIsA400NotAStackOverflow() { + for (body in listOf("""{"a":""".repeat(2_000) + "1" + "}".repeat(2_000), "[".repeat(20_000) + "]".repeat(20_000))) { + val answer = HttpRelayHandler(MemoryRelay(backend, { VerifyPolicy }, limits = null), origins = { listOf(origin) }).ask(HttpRelayCommand.REQ, body) + assertEquals(400, answer.status, body.take(20)) + } + } + + @Test + fun aFullAuthPolicyRefusesTransportSignInUntilItOptsIn() { + val body = """{"kinds":[1]}""" + val relayUrl = RelayUrlNormalizer.normalize("wss://relay.example") + val refusing = + MemoryRelay(backend, { + object : FullAuthPolicy(relayUrl) { + override suspend fun authorize(event: RelayAuthEvent): Unit = error("backend rejected user") + } + }) + val refused = HttpRelayHandler(refusing, origins = { listOf(origin) }).ask(HttpRelayCommand.REQ, body, token(HttpRelayCommand.REQ, body)) + assertEquals(403, refused.status, refused.lines.toString()) + assertTrue(refused.lines.single().startsWith("""["CLOSED","restricted:"""), refused.lines.toString()) + + val optingIn = + MemoryRelay(backend, { + object : FullAuthPolicy(relayUrl) { + override suspend fun authorizeTransport(pubkey: HexKey): String? = null + } + }) + val signed = HttpRelayHandler(optingIn, origins = { listOf(origin) }).ask(HttpRelayCommand.REQ, body, token(HttpRelayCommand.REQ, body)) + assertEquals(200, signed.status, signed.lines.toString()) + } + + @Test + fun aFloodOfFreshTokensCannotFlushAReplay() { + val h = HttpRelayHandler(MemoryRelay(backend, false), origins = { listOf(origin) }, verifier = Nip98AuthVerifier(maxReplayEntries = 4)) + + // A token's id is its hash, so tokens signed the same second over the same body are one token: + // every one here names its own body. + fun body(n: Int) = """{"kinds":[$n]}""" + val victim = token(HttpRelayCommand.REQ, body(0)) + assertEquals(200, h.ask(HttpRelayCommand.REQ, body(0), victim).status) + for (n in 1..3) assertEquals(200, h.ask(HttpRelayCommand.REQ, body(n), token(HttpRelayCommand.REQ, body(n))).status) + val flooding = h.ask(HttpRelayCommand.REQ, body(4), token(HttpRelayCommand.REQ, body(4))) + assertEquals(429, flooding.status, "a full cache refuses the new token: ${flooding.lines}") + val replayed = h.ask(HttpRelayCommand.REQ, body(0), victim) + assertEquals(401, replayed.status, "and still remembers the old one: ${replayed.lines}") + } + + @Test + fun aMessageLimitInThePolicyChainStillRuns() { + val limited = MemoryRelay(backend, { LimitsPolicy(RelayLimits(maxMessageLength = 4096)) + VerifyPolicy }, limits = null) + val answer = HttpRelayHandler(limited, origins = { listOf(origin) }).ask(HttpRelayCommand.REQ, """{"search":"${"x".repeat(20_000)}"}""") + assertEquals(400, answer.status, answer.lines.toString()) + assertTrue(answer.lines.single().startsWith("""["NOTICE","invalid: message too large"""), answer.lines.toString()) + } + + @Test + fun aMultiByteEventUnderTheCharacterLimitIsAccepted() { + // 1,500 CJK characters: about 4,500 UTF-8 bytes, well under 4,096 characters as the engine counts. + val posted = note("\u4E2D".repeat(1_500)) + val answer = handler().ask(HttpRelayCommand.EVENT, posted.toJson()) + assertEquals(200, answer.status, answer.lines.toString()) + } + + @Test + fun aBackendFailureIsA500Line() { + val failing = + object : SessionBackend by backend { + override suspend fun sealedNegentropyStorage( + filters: List<Filter>, + maxEntries: Int, + ) = error("db is down") + } + val message = Negentropy(StorageVector().also { it.seal() }, 0).initiate().toHexKey() + val answer = HttpRelayHandler(MemoryRelay(failing, { VerifyPolicy }), origins = { listOf(origin) }).ask(HttpRelayCommand.NEG, """[{"kinds":[1]},"$message"]""") + assertEquals(500, answer.status, answer.lines.toString()) + assertTrue(answer.lines.single().startsWith("""["CLOSED","error:"""), answer.lines.toString()) + } + + @Test + fun anInfiniteDeadlineStillStreams() { + backend.events += note("forever") + val answer = handler(deadline = Duration.INFINITE).ask(HttpRelayCommand.REQ, """{"kinds":[1]}""") + assertEquals(200, answer.status) + assertEquals("""["EOSE"]""", answer.lines.last()) + } + + @Test + fun aReaderStalledOnASingleAnswerIsDropped() { + val h = HttpRelayHandler(MemoryRelay(backend, false), origins = { listOf(origin) }, deadline = 100.milliseconds, tailGrace = 100.milliseconds) + val stalled = + object : HttpRelayResponse { + override suspend fun single( + status: Int, + frame: String, + ) = awaitCancellation() + + override suspend fun stream(lines: suspend HttpRelayLines.() -> Unit) = error("single") + } + assertFailsWith<HttpRelayReaderStalled> { + runBlocking { h.handle(HttpRelayRequest(HttpRelayCommand.COUNT, null, """{"kinds":[1]}""".encodeToByteArray()), stalled) } + } + } + + @Test + fun theBodyShapeIsReadWithoutATree() { + assertEquals("""["REQ","http",{"kinds":[1]}]""", HttpRelayCommand.REQ.frameOf(""" {"kinds":[1]} """)) + assertEquals("""["COUNT","http",{"a":"]"},{"b":"\"["}]""", HttpRelayCommand.COUNT.frameOf("""[{"a":"]"},{"b":"\"["}]""")) + assertEquals("""["NEG-OPEN","http",{"kinds":[1]},"61"]""", HttpRelayCommand.NEG.frameOf("""[{"kinds":[1]},"61"]""")) + assertEquals("""["EVENT",{"id":"x"}]""", HttpRelayCommand.EVENT.frameOf("""{"id":"x"}""")) + for (bad in listOf("", "[]", "{", "[{}", "{}}", "{]", "[{}]x", "{} {}", "[{},]", "[,{}]", "[{} {}]", "[1]", """[{},"x"]""", "null", "\"x\"")) { + assertEquals(null, HttpRelayCommand.REQ.frameOf(bad), "REQ '$bad'") + } + for (bad in listOf("""[{"kinds":[1]}]""", """["61",{}]""", """[{},61]""", """[{},"61",1]""", """{"kinds":[1]}""")) { + assertEquals(null, HttpRelayCommand.NEG.frameOf(bad), "NEG '$bad'") + } + for (bad in listOf("""[{"id":"x"}]""", "1")) assertEquals(null, HttpRelayCommand.EVENT.frameOf(bad), "EVENT '$bad'") + } + private companion object { const val STALLED_KIND = 7 const val TRICKLE_KIND = 8 From ca7ffd2ad21ff0eab5bf8225770b90b050734b6b Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sun, 27 Sep 2026 03:07:39 +0000 Subject: [PATCH 36/60] fix(quartz): give search the release notes, list roles and caption words it was missing Three gaps between what a kind's indexableContent() says and what a field-weighting search engine got from SearchFieldExtractor. Kind 30063: the NIP-82 release notes reached indexableContent() but not the extractor, whose branch passed title and description only. So an engine reading the extractor could not find a release by what changed in it, while SQLiteEventStore could. searchableReleaseNotes() becomes public and the branch passes it as the body. The NIP-51 encrypted-content guard lives in that one function, so both paths apply the same check. Kinds 9998/39998/9999/39999 (Decentralized Lists): no branch, so the catch-all put the whole blob in the body, titles included. The `t` values it appends were ALSO carried by the funnel's hashtag role, so every list hashtag was indexed twice, the doubling already removed for 1111/1311/30382. One `is DecentralizedListEvent` branch now puts names and titles in the title tier and description and comments in the secondary tier, and leaves hashtags to the funnel. The role helpers sit beside forEachSearchableListField in SearchExt.kt so the two stay in sync. Kind 39307 (text tracks): the whole WebVTT document was indexed, so the header, every `-->` timing line, cue settings, ids, NOTE/STYLE blocks and inline tags sat in the index beside the dialogue. A search for "00" or "align" matched every caption. WebVttText reads the cue payload lines in one pass, drops tags, and decodes character references. SRT fits the same shape. Content with no timing line is not a caption file and is indexed as written. indexableContent() and forEachIndexableField() both go through it, so the SQLite FTS index is fixed too. Stores should run reindexFullTextSearch() for 39307; the searchable-kinds table is updated. Tests: SearchFieldExtractorTest covers both 30063 shapes, a list header, a list item's single-counted hashtags and a text track. WebVttTextTest covers cue selection, markup, references, CRLF/SRT, the visitor rejoin contract and early stop. The golden file is unchanged: its fixture has no timing line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdSGU88PYrRVyVHjiEc5sw --- .../references/searchable-kinds.md | 2 +- .../decentralizedLists/SearchExt.kt | 19 ++ .../nip50Search/SearchFieldExtractor.kt | 19 +- .../ReleaseArtifactSetEvent.kt | 12 +- .../nip71Video/textTrack/TextTrackEvent.kt | 10 +- .../quartz/nip71Video/textTrack/WebVttText.kt | 219 ++++++++++++++++++ .../nip50Search/SearchFieldExtractorTest.kt | 71 ++++++ .../nip71Video/textTrack/WebVttTextTest.kt | 111 +++++++++ 8 files changed, 454 insertions(+), 9 deletions(-) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/textTrack/WebVttText.kt create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip71Video/textTrack/WebVttTextTest.kt diff --git a/.claude/skills/searchable-events/references/searchable-kinds.md b/.claude/skills/searchable-events/references/searchable-kinds.md index d9942a6530..532ecc3d68 100644 --- a/.claude/skills/searchable-events/references/searchable-kinds.md +++ b/.claude/skills/searchable-events/references/searchable-kinds.md @@ -140,7 +140,7 @@ Separator legend: **NL** = `joinToString("\n")`, **SP** = `joinToString(" ")`. | 39000 | GroupMetadataEvent | nip29RelayGroups/metadata | `listOfNotNull(name(), about())` NL | | 39089 | StarterPackEvent | nip51Lists/starterPack | `listOfNotNull(title(), description())` NL | | 39092 | MediaStarterPackEvent | nip51Lists/mediaStarterPack | `listOfNotNull(title(), description())` NL | -| 39307 | TextTrackEvent | nip71Video/textTrack | `content` (the WebVTT cue text) | +| 39307 | TextTrackEvent | nip71Video/textTrack | `WebVttText.cueText(content)` NL — each cue's payload lines with tags removed and character references decoded; no header, timings, cue settings, ids, `NOTE`/`STYLE`/`REGION` blocks. Content with no `-->` timing line is not a caption file and is indexed as written | | 39701 | WebBookmarkEvent | nipB0WebBookmarks | `listOfNotNull(title(), description())` NL | | 39998 | AddressableListHeaderEvent | experimental/decentralizedLists/header | same as 9998 | | 39999 | AddressableListItemEvent | experimental/decentralizedLists/item | same as 9998 | diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/SearchExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/SearchExt.kt index e866f645b9..27557c2bbd 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/SearchExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/decentralizedLists/SearchExt.kt @@ -20,12 +20,17 @@ */ package com.vitorpamplona.quartz.experimental.decentralizedLists +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.names import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.NamesTag import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.SingularPlural import com.vitorpamplona.quartz.experimental.decentralizedLists.header.tags.TitlesTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.titles +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.comments +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.name import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.CommentsTag import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.NameTag import com.vitorpamplona.quartz.experimental.decentralizedLists.item.tags.TitleTag +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.title import com.vitorpamplona.quartz.experimental.decentralizedLists.tags.DescriptionTag import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.core.fastForEach @@ -84,6 +89,20 @@ fun TagArray.forEachSearchableListField(visitor: IndexableFieldVisitor): Boolean return true } +/** + * The TITLE role of [forEachSearchableListField], for search engines that weight fields: what the + * list or item is called — the header's `names` and `titles` (singular, then plural) and the + * item's `name` and `title`. Same first-well-formed-tag rule as the walk. + */ +fun TagArray.searchableListTitles(): List<String?> { + val names = names() + val titles = titles() + return listOf(names?.singular, names?.plural, titles?.singular, titles?.plural, name(), title()) +} + +/** The DESCRIPTION role of [forEachSearchableListField]: `description`, then `comments`. */ +fun TagArray.searchableListDescriptions(): List<String?> = listOf(description(), comments()) + /** The write-path join of [forEachSearchableListField]: one field per line. */ fun TagArray.searchableListContent() = buildString { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractor.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractor.kt index 139530d089..b9a679d8b6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractor.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractor.kt @@ -29,6 +29,9 @@ import com.vitorpamplona.quartz.experimental.agora.FundraiserEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.birdstar.BirdDetectionEvent import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.DecentralizedListEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.searchableListDescriptions +import com.vitorpamplona.quartz.experimental.decentralizedLists.searchableListTitles import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.fitness.workout.ExerciseTemplateEvent import com.vitorpamplona.quartz.experimental.fitness.workout.WorkoutRecordEvent @@ -380,8 +383,11 @@ object SearchFieldExtractor { tiers(event, event.title(), event.description(), null) } + // kind 30063 -- the body is a NIP-82 release's notes, and nothing for a + // NIP-51 set (its content may be encrypted private items); the event + // owns that guard, so both search paths apply the same one. is ReleaseArtifactSetEvent -> { - tiers(event, event.title(), event.description(), null) + tiers(event, event.title(), event.description(), event.searchableReleaseNotes()) } is AppCurationSetEvent -> { @@ -617,6 +623,17 @@ object SearchFieldExtractor { tiers(event, null, null, event.content) } + // kinds 9998/39998/9999/39999 -- a list's or item's names and titles are + // what it is called, its description and comments what it is about. The + // `t` values indexableContent() appends are the funnel's hashtag role + // already, so -- as for 1111/1311/30382 -- they are not passed again, and + // content is not part of the spec. One branch for all four: an item may + // carry header tags (the spec's nonstandard method), and the tag helpers + // read whichever are present. + is DecentralizedListEvent -> { + tiers(event, event.tags.searchableListTitles(), event.tags.searchableListDescriptions(), null) + } + // kind 1 LAST among the explicit branches, defensively: a future // kind extending the text-note base must hit its own branch first. is TextNoteEvent -> { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt index 8926ed25cb..bd151437b0 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt @@ -81,10 +81,14 @@ class ReleaseArtifactSetEvent( visitor.visit(searchableReleaseNotes()) } - // Only NIP-82 releases carry release notes in `content`. A NIP-51 set may keep - // encrypted private items there, which must never reach the index. The blank - // check runs first so empty sets skip the two tag scans on every search. - private fun searchableReleaseNotes() = if (content.isNotBlank() && isNip82SoftwareRelease()) content else null + /** + * The release notes when they are safe to index, else null. Only NIP-82 releases carry + * release notes in `content`; a NIP-51 set may keep encrypted private items there, which + * must never reach the index. The blank check runs first so empty sets skip the two tag + * scans on every search. `SearchFieldExtractor` reads this too, so both search paths + * apply the same guard. + */ + fun searchableReleaseNotes() = if (content.isNotBlank() && isNip82SoftwareRelease()) content else null override fun eventHints() = tags.mapNotNull(EventBookmark::parseAsHint) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/textTrack/TextTrackEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/textTrack/TextTrackEvent.kt index 91e0acab39..e9e31c9e13 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/textTrack/TextTrackEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/textTrack/TextTrackEvent.kt @@ -59,11 +59,15 @@ class TextTrackEvent( AddressHintProvider, SearchableEvent { // The cue text is the only searchable thing here: the `d` tag is a slug and the rest is - // plumbing. Indexing it makes a video findable by what is said in it. - override fun indexableContent() = content + // plumbing. Indexing it makes a video findable by what is said in it — the words only, + // not the timings, cue settings and markup around them (see WebVttText). Content with no + // cue at all is not a caption file and is indexed as written. + override fun indexableContent() = if (WebVttText.hasCues(content)) WebVttText.cueText(content) else content + // The read path: the same lines cueText() joins, one visit per line so a hit early in the + // track stops the walk before the rest is cleaned. override fun forEachIndexableField(visitor: IndexableFieldVisitor) { - visitor.visit(content) + if (WebVttText.hasCues(content)) WebVttText.forEachCueLine(content, visitor) else visitor.visit(content) } override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/textTrack/WebVttText.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/textTrack/WebVttText.kt new file mode 100644 index 0000000000..9fdf875374 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/textTrack/WebVttText.kt @@ -0,0 +1,219 @@ +/* + * 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.quartz.nip71Video.textTrack + +import com.vitorpamplona.quartz.nip50Search.IndexableFieldVisitor + +/** + * The words a WebVTT document says: every cue's payload lines with their markup removed, and + * nothing else. + * + * Search is the reason this exists. A text track's `content` is the whole document, so indexing + * it raw puts `WEBVTT`, every `00:01:02.500 --> 00:01:04.000` timing line, cue settings + * (`align:start`), cue ids, `NOTE` and `STYLE` blocks, and inline tags (`<v Roger>`, `<i>`, + * `<00:00:01.000>`) into the index beside the dialogue. A query for "00" or "align" then + * matches every caption ever published, and the terms that matter are diluted by markup. + * + * ## What counts as a cue line + * + * One pass over the lines, no model built. A line containing `-->` opens a cue (it is the + * timing line; the optional identifier line before it is skipped because nothing is open yet), + * and every non-blank line after it until the next blank line is payload. Everything outside a + * cue — the `WEBVTT` header, `NOTE`, `STYLE`, `REGION` — is skipped by the same rule, since none + * of it may contain `-->`. SRT happens to fit the same shape (a numeric id, a timing line, the + * text), so a publisher that wraps SRT instead of WebVTT is read correctly too. + * + * A document with no timing line at all is not a caption file, so it is indexed as written + * rather than discarded: [hasCues] is the switch, and callers pass such content through + * unchanged. + * + * ## Markup + * + * Inline tags are removed, not their contents: `<i>really</i>` is `really`. Voice spans lose the + * speaker name with the tag (`<v Roger>Hello` is `Hello`): the name is an annotation, not + * something said. The HTML character references WebVTT allows are decoded — the named ones the + * spec lists, plus numeric ones — so `Tom & Jerry` is indexed as `Tom & Jerry`. An unknown + * reference is kept as written. + */ +object WebVttText { + private const val TIMING_ARROW = "-->" + + /** Whether [document] has at least one cue. Without one it is not a caption file. */ + fun hasCues(document: String) = document.contains(TIMING_ARROW) + + /** + * The cue text of [document], one payload line per line. Callers should check [hasCues] + * first; a document without cues yields an empty string here. + */ + fun cueText(document: String): String { + val out = StringBuilder(document.length) + forEachCueLine(document) { line -> + if (out.isNotEmpty()) out.append('\n') + out.append(line) + true + } + return out.toString() + } + + /** + * Hands each cleaned, non-empty payload line of [document] to [visitor] in order. Joining + * them with `"\n"` gives exactly [cueText]. Returns false when the visitor stopped the walk. + * + * Lines without markup are passed as substrings; only a line carrying a tag or a character + * reference is rebuilt. + */ + fun forEachCueLine( + document: String, + visitor: IndexableFieldVisitor, + ): Boolean { + var inCue = false + var start = 0 + val length = document.length + while (start <= length) { + var end = start + while (end < length && document[end] != '\n' && document[end] != '\r') end++ + + if (isBlank(document, start, end)) { + inCue = false + } else if (!inCue) { + // Header, NOTE / STYLE / REGION blocks and cue identifiers all end up here and + // are skipped; only a timing line opens a cue. + if (hasArrow(document, start, end)) inCue = true + } else { + val line = clean(document, start, end) + if (line != null && !visitor.visit(line)) return false + } + + if (end >= length) break + // A CRLF pair is one line break. + start = if (document[end] == '\r' && end + 1 < length && document[end + 1] == '\n') end + 2 else end + 1 + } + return true + } + + /** Bounded to the line: an unbounded indexOf would rescan up to the next cue from every skipped line. */ + private fun hasArrow( + s: String, + start: Int, + end: Int, + ): Boolean { + for (i in start until end - 2) { + if (s[i] == '-' && s[i + 1] == '-' && s[i + 2] == '>') return true + } + return false + } + + private fun isBlank( + s: String, + start: Int, + end: Int, + ): Boolean { + for (i in start until end) if (!s[i].isWhitespace()) return false + return true + } + + /** The payload line in `[start, end)` without tags, references decoded and trimmed; null when nothing is left. */ + private fun clean( + s: String, + start: Int, + end: Int, + ): String? { + var markup = false + for (i in start until end) { + val c = s[i] + if (c == '<' || c == '&') { + markup = true + break + } + } + if (!markup) return s.substring(start, end).trim().ifEmpty { null } + + val out = StringBuilder(end - start) + var i = start + while (i < end) { + val c = s[i] + when (c) { + '<' -> { + val close = s.indexOf('>', i + 1) + // An unterminated tag runs to the end of the line, as the WebVTT parser reads it. + i = if (close < 0 || close >= end) end else close + 1 + } + + '&' -> { + val semi = s.indexOf(';', i + 1) + val decoded = if (semi in (i + 1) until end) decodeReference(s, i + 1, semi) else null + if (decoded == null) { + out.append(c) + i++ + } else { + appendCodePoint(out, decoded) + i = semi + 1 + } + } + + else -> { + out.append(c) + i++ + } + } + } + return out.toString().trim().ifEmpty { null } + } + + /** The code point named by the reference body in `[start, end)` (between `&` and `;`), or null. */ + private fun decodeReference( + s: String, + start: Int, + end: Int, + ): Int? { + if (end - start > 10) return null + if (s[start] == '#') { + val hex = start + 1 < end && (s[start + 1] == 'x' || s[start + 1] == 'X') + val digits = s.substring(if (hex) start + 2 else start + 1, end) + val code = digits.toIntOrNull(if (hex) 16 else 10) ?: return null + return if (code in 1..0x10FFFF && code !in 0xD800..0xDFFF) code else null + } + return when (s.substring(start, end)) { + "amp" -> '&'.code + "lt" -> '<'.code + "gt" -> '>'.code + "quot" -> '"'.code + "apos" -> '\''.code + "nbsp" -> 0x00A0 + "lrm" -> 0x200E + "rlm" -> 0x200F + else -> null + } + } + + private fun appendCodePoint( + out: StringBuilder, + code: Int, + ) { + if (code <= 0xFFFF) { + out.append(code.toChar()) + } else { + val v = code - 0x10000 + out.append((0xD800 + (v shr 10)).toChar()) + out.append((0xDC00 + (v and 0x3FF)).toChar()) + } + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractorTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractorTest.kt index 51b77ca13f..617a4fe607 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractorTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip50Search/SearchFieldExtractorTest.kt @@ -23,6 +23,8 @@ package com.vitorpamplona.quartz.nip50Search import com.vitorpamplona.quartz.buzz.agentProfiles.AgentProfileEvent import com.vitorpamplona.quartz.experimental.birdstar.BirdDetectionEvent import com.vitorpamplona.quartz.experimental.birdstar.BirdexEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.header.ListHeaderEvent +import com.vitorpamplona.quartz.experimental.decentralizedLists.item.AddressableListItemEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.experimental.ps1saves.Ps1SaveEvent import com.vitorpamplona.quartz.experimental.trustedLists.users.UserTrustedListEvent @@ -38,9 +40,11 @@ import com.vitorpamplona.quartz.nip29RelayGroups.moderation.GroupEditMetadataEve import com.vitorpamplona.quartz.nip32Labeling.LabelEvent import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent +import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent import com.vitorpamplona.quartz.nip69P2pOrderEvents.P2POrderEvent +import com.vitorpamplona.quartz.nip71Video.textTrack.TextTrackEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.users.UserAssertionEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent @@ -412,6 +416,73 @@ class SearchFieldExtractorTest { assertEquals(IndexableFields.Tiered(secondary = listOf("spam"), text = "obvious bot"), fields) } + @Test + fun nip82ReleaseNotesAreTheBodyOfARelease() { + // indexableContent() already carried the notes; the branch used to drop them, so an + // engine reading this extractor could not find a release by what changed in it. + val tags = arrayOf(arrayOf("d", "com.example.app@1.2.0"), arrayOf("i", "com.example.app"), arrayOf("version", "1.2.0"), arrayOf("title", "Example 1.2")) + val fields = SearchFieldExtractor.extract(ReleaseArtifactSetEvent("2e".repeat(32), alice, 1L, tags, "fixes the offline sync crash", "")) + assertEquals(IndexableFields.Tiered(primary = listOf("Example 1.2"), text = "fixes the offline sync crash"), fields) + } + + @Test + fun nip51ReleaseSetContentIsNeverTheBody() { + // A NIP-51 set (no `i` + `version`) may keep encrypted private items in content. + val tags = arrayOf(arrayOf("d", "release"), arrayOf("title", "Example"), arrayOf("description", "all builds")) + val fields = SearchFieldExtractor.extract(ReleaseArtifactSetEvent("2f".repeat(32), alice, 1L, tags, "AgJ0b3BzZWNyZXQ=", "")) + assertEquals(IndexableFields.Tiered(primary = listOf("Example"), secondary = listOf("all builds")), fields) + } + + @Test + fun decentralizedListHeadersSplitWhatTheyAreCalledFromWhatTheyAreAbout() { + val tags = + arrayOf( + arrayOf("names", "podcast", "podcasts"), + arrayOf("titles", "Podcast", "Podcasts"), + arrayOf("description", "shows worth a listen"), + arrayOf("t", "audio"), + ) + val fields = SearchFieldExtractor.extract(ListHeaderEvent("3a".repeat(32), alice, 1L, tags, "", "")) + assertEquals( + IndexableFields.Tiered( + primary = listOf("podcast", "podcasts", "Podcast", "Podcasts"), + secondary = listOf("shows worth a listen"), + hashtags = listOf("audio"), + ), + fields, + ) + } + + @Test + fun decentralizedListItemHashtagsAreNotIndexedTwice() { + // indexableContent() appends the `t` values; the funnel already carries them as + // hashtags, so the branch leaves them out of every tier (the 1111 reasoning). + val tags = + arrayOf( + arrayOf("z", "podcasts"), + arrayOf("title", "Bitcoin Audible"), + arrayOf("description", "long reads, read aloud"), + arrayOf("comments", "start with the classics"), + arrayOf("t", "bitcoin"), + ) + val fields = SearchFieldExtractor.extract(AddressableListItemEvent("3b".repeat(32), alice, 1L, tags + arrayOf(arrayOf("d", "ba")), "", "")) + assertEquals( + IndexableFields.Tiered( + primary = listOf("Bitcoin Audible"), + secondary = listOf("long reads, read aloud", "start with the classics"), + hashtags = listOf("bitcoin"), + ), + fields, + ) + } + + @Test + fun textTracksIndexWhatIsSaidNotTheTimings() { + val vtt = "WEBVTT\n\n1\n00:00:00.000 --> 00:00:02.000 align:start\n<v Roger>Hello nostr\n" + val fields = SearchFieldExtractor.extract(TextTrackEvent("3c".repeat(32), alice, 1L, arrayOf(arrayOf("d", "subtitles:v1")), vtt, "")) + assertEquals(IndexableFields.Tiered(text = "Hello nostr"), fields) + } + @Test fun commentHashtagsAreNotIndexedTwice() { // indexableContent() concatenates the `t` tags INTO the body for kind diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip71Video/textTrack/WebVttTextTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip71Video/textTrack/WebVttTextTest.kt new file mode 100644 index 0000000000..e435eef505 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip71Video/textTrack/WebVttTextTest.kt @@ -0,0 +1,111 @@ +/* + * 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.quartz.nip71Video.textTrack + +import com.vitorpamplona.quartz.nip50Search.IndexableFieldVisitor +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class WebVttTextTest { + private val document = + """ + WEBVTT - the whole header line is skipped + Kind: captions + + STYLE + ::cue { color: yellow } + + NOTE this block is a comment + and so is this line + + intro + 00:00:00.000 --> 00:00:02.000 align:start position:10% + <v Roger Bingham>We are in New York City + + 00:00:02.500 --> 00:00:04.000 + <i>Really</i>? Tom & Jerry! + <c.loud>on <00:00:03.000>two lines</c> + + 00:00:05.000 --> 00:00:06.000 + <b></b> + """.trimIndent() + + @Test + fun keepsOnlyTheWordsOfEachCue() { + assertEquals("We are in New York City\nReally? Tom & Jerry!\non two lines", WebVttText.cueText(document)) + } + + @Test + fun theVisitedLinesRejoinToTheCueText() { + val lines = mutableListOf<String>() + assertTrue(WebVttText.forEachCueLine(document, IndexableFieldVisitor { lines.add(it!!) })) + assertEquals(WebVttText.cueText(document), lines.joinToString("\n")) + } + + @Test + fun aVisitorCanStopTheWalk() { + val lines = mutableListOf<String>() + assertFalse( + WebVttText.forEachCueLine(document, IndexableFieldVisitor { lines.add(it!!) && false }), + ) + assertEquals(listOf("We are in New York City"), lines) + } + + @Test + fun crlfAndSrtAreReadTheSameWay() { + val srt = "1\r\n00:00:01,000 --> 00:00:02,000\r\nHello\r\n\r\n2\r\n00:00:03,000 --> 00:00:04,000\r\nworld\r\n" + assertEquals("Hello\nworld", WebVttText.cueText(srt)) + } + + @Test + fun unknownReferencesAndStrayAmpersandsStayAsWritten() { + val vtt = "WEBVTT\n\n00:00.000 --> 00:01.000\nfish & chips &unknown; &#xZZ;" + assertEquals("fish & chips &unknown; &#xZZ;", WebVttText.cueText(vtt)) + } + + @Test + fun anUnterminatedTagRunsToTheEndOfTheLine() { + val vtt = "WEBVTT\n\n00:00.000 --> 00:01.000\nkept <i unterminated\nnext line" + assertEquals("kept\nnext line", WebVttText.cueText(vtt)) + } + + @Test + fun plainTextIsNotACaptionFile() { + assertFalse(WebVttText.hasCues("just a description of the track")) + assertTrue(WebVttText.hasCues(document)) + } + + @Test + fun theEventIndexesCueTextAndPassesPlainContentThrough() { + val tags = arrayOf(arrayOf("d", "subtitles:v1")) + val track = TextTrackEvent("1".repeat(64), "a1".repeat(32), 1L, tags, document, "") + assertEquals(WebVttText.cueText(document), track.indexableContent()) + + val visited = mutableListOf<String?>() + track.forEachIndexableField { visited.add(it) } + assertEquals(track.indexableContent(), visited.joinToString(track.indexableSeparator())) + + val plain = TextTrackEvent("2".repeat(64), "a1".repeat(32), 1L, tags, "no cues here", "") + assertEquals("no cues here", plain.indexableContent()) + } +} From d7ca63008a051f0fa577397618996c581a7c446f Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sun, 27 Sep 2026 03:27:57 +0000 Subject: [PATCH 37/60] refactor(chess): one shared ChessViewModel for Android and Desktop ChessViewModelNew (Android) and DesktopChessViewModelNew forwarded the same ~40 members to ChessLobbyLogic; only the adapters and the scope differed. commons/nip64Chess/ChessViewModel now takes the adapters, the polling config, the dismissed-games store and an optional scope. - Android: ChessViewModelFactory builds it with the Android adapters and no scope, so it runs on viewModelScope and stops polling in onCleared, as before. The factory's unused Application parameter is gone. - Desktop: ChessScreen builds it with the Desktop adapters and its rememberCoroutineScope(); the UserMetadataCache the old class owned is remembered beside it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkG4KTqmnprUK9BAcwAyc8 --- .../amethyst/ui/note/types/Chess.kt | 8 +- .../screen/loggedIn/chess/ChessGameScreen.kt | 11 +- .../screen/loggedIn/chess/ChessLobbyScreen.kt | 17 +- .../loggedIn/chess/ChessRelaySettingsSheet.kt | 5 +- .../loggedIn/chess/ChessViewModelFactory.kt | 18 +- .../chess/datasource/ChessSubscription.kt | 4 +- .../loggedIn/home/NewChessGameButton.kt | 8 +- .../commons/nip64Chess/ChessViewModel.kt | 59 +++-- .../amethyst/desktop/chess/ChessScreen.kt | 32 ++- .../desktop/chess/DesktopChessViewModelNew.kt | 203 ------------------ 10 files changed, 86 insertions(+), 279 deletions(-) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelNew.kt => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessViewModel.kt (77%) delete mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/DesktopChessViewModelNew.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt index 7f7c919335..a9907ff6f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt @@ -48,6 +48,7 @@ import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.navigation.routeFor import com.vitorpamplona.amethyst.commons.nip64Chess.ChessChallenge +import com.vitorpamplona.amethyst.commons.nip64Chess.ChessViewModel import com.vitorpamplona.amethyst.commons.nip64Chess.ui.ChessGameViewer import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.chess_accept @@ -61,7 +62,6 @@ import com.vitorpamplona.amethyst.ui.note.UsernameDisplay 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.chess.ChessViewModelFactory -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessViewModelNew import com.vitorpamplona.quartz.nip64Chess.challenge.offer.LiveChessGameChallengeEvent import com.vitorpamplona.quartz.nip64Chess.end.LiveChessGameEndEvent import com.vitorpamplona.quartz.nip64Chess.game.ChessGameEvent @@ -124,10 +124,10 @@ fun RenderLiveChessChallenge( val gameId = event.gameId() val activity = LocalActivity.current as androidx.fragment.app.FragmentActivity - val chessViewModel: ChessViewModelNew = + val chessViewModel: ChessViewModel = viewModel( - key = "ChessViewModelNew-${accountViewModel.account.userProfile().pubkeyHex}", - factory = ChessViewModelFactory(accountViewModel.account, activity.application), + key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}", + factory = ChessViewModelFactory(accountViewModel.account), ) val isOpenChallenge = event.opponentPubkey() == null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt index d013cb4f1c..2e15512276 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessGameScreen.kt @@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.nip64Chess.ChessBroadcastStatus +import com.vitorpamplona.amethyst.commons.nip64Chess.ChessViewModel import com.vitorpamplona.amethyst.commons.nip64Chess.ui.ChessBroadcastBanner import com.vitorpamplona.amethyst.commons.nip64Chess.ui.ChessSyncBanner import com.vitorpamplona.amethyst.commons.nip64Chess.ui.LiveChessGameScreen @@ -99,15 +100,11 @@ fun ChessGameScreen( ) { // Scope ViewModel to Activity so it's shared between lobby and game screens val activity = LocalActivity.current as FragmentActivity - val chessViewModel: ChessViewModelNew = + val chessViewModel: ChessViewModel = viewModel( viewModelStoreOwner = activity, - key = "ChessViewModelNew-${accountViewModel.account.userProfile().pubkeyHex}", - factory = - ChessViewModelFactory( - accountViewModel.account, - activity.application, - ), + key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}", + factory = ChessViewModelFactory(accountViewModel.account), ) val activeGames by chessViewModel.activeGames.collectAsState() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt index 811cbb1c66..ca57f5d7c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt @@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.nip64Chess.ChessChallenge +import com.vitorpamplona.amethyst.commons.nip64Chess.ChessViewModel import com.vitorpamplona.amethyst.commons.nip64Chess.ui.ActiveGameCard import com.vitorpamplona.amethyst.commons.nip64Chess.ui.ChallengeCard import com.vitorpamplona.amethyst.commons.nip64Chess.ui.ChessSyncBanner @@ -97,11 +98,11 @@ fun ChessLobbyScreen( ) { // Scope ViewModel to Activity so it's shared between lobby and game screens val activity = LocalActivity.current as FragmentActivity - val chessViewModel: ChessViewModelNew = + val chessViewModel: ChessViewModel = viewModel( viewModelStoreOwner = activity, - key = "ChessViewModelNew-${accountViewModel.account.userProfile().pubkeyHex}", - factory = ChessViewModelFactory(accountViewModel.account, activity.application), + key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}", + factory = ChessViewModelFactory(accountViewModel.account), ) // Subscribe to chess events when screen is visible @@ -133,7 +134,7 @@ fun ChessLobbyScreen( @Composable fun NavigateIfInAGame( - chessViewModel: ChessViewModelNew, + chessViewModel: ChessViewModel, nav: INav, ) { val selectedGameId by chessViewModel.selectedGameId.collectAsState() @@ -156,7 +157,7 @@ fun NavigateIfInAGame( @OptIn(ExperimentalMaterial3Api::class) @Composable fun ChessLobbyScreen( - chessViewModel: ChessViewModelNew, + chessViewModel: ChessViewModel, accountViewModel: AccountViewModel, nav: INav, ) { @@ -256,7 +257,7 @@ fun ChessLobbyScreen( } @Composable -fun ChessSyncBanner(chessViewModel: ChessViewModelNew) { +fun ChessSyncBanner(chessViewModel: ChessViewModel) { val syncStatus by chessViewModel.syncStatus.collectAsState() ChessSyncBanner( @@ -267,7 +268,7 @@ fun ChessSyncBanner(chessViewModel: ChessViewModelNew) { } @Composable -fun ErrorDisplay(chessViewModel: ChessViewModelNew) { +fun ErrorDisplay(chessViewModel: ChessViewModel) { // Error display val error by chessViewModel.error.collectAsState() @@ -292,7 +293,7 @@ fun ErrorDisplay(chessViewModel: ChessViewModelNew) { @Composable fun ChessLobbyContent( - chessViewModel: ChessViewModelNew, + chessViewModel: ChessViewModel, accountViewModel: AccountViewModel, onAcceptChallenge: (ChessChallenge) -> Unit, onOpenOwnChallenge: (ChessChallenge) -> Unit, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.kt index d1069687de..faa4bff105 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessRelaySettingsSheet.kt @@ -46,6 +46,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.nip64Chess.ChessConfig +import com.vitorpamplona.amethyst.commons.nip64Chess.ChessViewModel import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.connected import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -59,7 +60,7 @@ import kotlinx.coroutines.flow.map */ @Composable fun ChessRelaySettingsSheet( - chessViewModel: ChessViewModelNew, + chessViewModel: ChessViewModel, accountViewModel: AccountViewModel, ) { // Get relay information for settings display @@ -226,7 +227,7 @@ fun ChessRelaySettingsSheet( @SuppressLint("StateFlowValueCalledInComposition") @Composable -fun StatRow(chessViewModel: ChessViewModelNew) { +fun StatRow(chessViewModel: ChessViewModel) { Column(horizontalAlignment = Alignment.CenterHorizontally) { val challengeCount by remember { chessViewModel.challenges.map { it.size } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt index ea1e191647..3d69227fd1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelFactory.kt @@ -22,20 +22,28 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chess import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.nip64Chess.ChessDismissedGamesStore +import com.vitorpamplona.amethyst.commons.nip64Chess.ChessPollingDefaults +import com.vitorpamplona.amethyst.commons.nip64Chess.ChessViewModel import com.vitorpamplona.amethyst.model.Account /** - * Factory for creating ChessViewModelNew instances. - * Uses the slim ViewModel that delegates to shared ChessLobbyLogic. + * Factory for the shared [ChessViewModel], wired with the Android adapters. */ class ChessViewModelFactory( private val account: Account, - private val application: android.app.Application, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { - if (modelClass.isAssignableFrom(ChessViewModelNew::class.java)) { - return ChessViewModelNew(account, application) as T + if (modelClass.isAssignableFrom(ChessViewModel::class.java)) { + return ChessViewModel( + userPubkey = account.userProfile().pubkeyHex, + publisher = AndroidChessPublisher(account), + fetcher = AndroidRelayFetcher(account), + metadataProvider = AndroidMetadataProvider(), + pollingConfig = ChessPollingDefaults.android, + dismissedStorage = ChessDismissedGamesStore(chessDismissedGamesData), + ) as T } throw IllegalArgumentException("Unknown ViewModel class") } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/datasource/ChessSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/datasource/ChessSubscription.kt index d8d788c180..a9522ae2e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/datasource/ChessSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/datasource/ChessSubscription.kt @@ -25,10 +25,10 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.nip64Chess.ChessViewModel import com.vitorpamplona.amethyst.commons.relayClient.chess.ChessQueryState import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessViewModelNew /** * Subscribe to chess events when the Chess screen is active. @@ -38,7 +38,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessViewModelNew */ @Composable fun ChessSubscription( - chessViewModel: ChessViewModelNew, + chessViewModel: ChessViewModel, accountViewModel: AccountViewModel, ) { // Get active game IDs from the view model for game-specific subscriptions diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewChessGameButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewChessGameButton.kt index 7c8c77eb6f..df0959e00b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewChessGameButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewChessGameButton.kt @@ -32,6 +32,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.nip64Chess.ChessViewModel import com.vitorpamplona.amethyst.commons.nip64Chess.ui.NewChessGameDialog import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.new_chess_game @@ -40,7 +41,6 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessViewModelFactory -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessViewModelNew /** * Floating action button for creating new chess game challenges @@ -53,10 +53,10 @@ fun NewChessGameButton( var showDialog by remember { mutableStateOf(false) } val activity = LocalActivity.current as androidx.fragment.app.FragmentActivity - val chessViewModel: ChessViewModelNew = + val chessViewModel: ChessViewModel = viewModel( - key = "ChessViewModelNew-${accountViewModel.account.userProfile().pubkeyHex}", - factory = ChessViewModelFactory(accountViewModel.account, activity.application), + key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}", + factory = ChessViewModelFactory(accountViewModel.account), ) FloatingActionButton( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelNew.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessViewModel.kt similarity index 77% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelNew.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessViewModel.kt index 9bd3819de4..bbd7695555 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessViewModelNew.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip64Chess/ChessViewModel.kt @@ -18,60 +18,51 @@ * 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.chess +package com.vitorpamplona.amethyst.commons.nip64Chess import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.commons.nip64Chess.ChessBroadcastStatus -import com.vitorpamplona.amethyst.commons.nip64Chess.ChessChallenge -import com.vitorpamplona.amethyst.commons.nip64Chess.ChessDismissedGamesStore -import com.vitorpamplona.amethyst.commons.nip64Chess.ChessLobbyLogic -import com.vitorpamplona.amethyst.commons.nip64Chess.ChessPollingDefaults -import com.vitorpamplona.amethyst.commons.nip64Chess.ChessSyncStatus -import com.vitorpamplona.amethyst.commons.nip64Chess.CompletedGame -import com.vitorpamplona.amethyst.commons.nip64Chess.PublicGame -import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip64Chess.Color import com.vitorpamplona.quartz.nip64Chess.LiveChessGameState import com.vitorpamplona.quartz.nip64Chess.jester.JesterProtocol import com.vitorpamplona.quartz.nip64Chess.jester.toJesterEvent import com.vitorpamplona.quartz.utils.Log +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow /** - * Slim Android ViewModel for chess (~120 lines). + * The chess screen's state holder, shared by Android and Desktop. * - * Delegates all business logic to ChessLobbyLogic. - * Only handles Android-specific concerns: - * - ViewModel lifecycle (viewModelScope) - * - Platform adapter creation - * - State exposure to Compose UI + * All business logic lives in [ChessLobbyLogic]; this class only exposes its state and forwards + * the user's actions. Each platform supplies its own adapters (how to publish, fetch, resolve + * metadata and remember dismissed games) and its polling cadence. + * + * On Android it is a lifecycle-scoped `ViewModel`: [scope] defaults to `viewModelScope` and + * polling stops in [onCleared]. Desktop, which has no `ViewModelStore`, passes the scope of the + * composition that owns it; polling then ends when that scope is cancelled. */ @Stable -class ChessViewModelNew( - private val account: Account, - application: android.app.Application, +class ChessViewModel( + private val userPubkey: HexKey, + publisher: ChessEventPublisher, + fetcher: ChessRelayFetcher, + metadataProvider: IUserMetadataProvider, + pollingConfig: ChessPollingConfig, + dismissedStorage: ChessDismissedGamesStore?, + scope: CoroutineScope? = null, ) : ViewModel() { - // Instance ID for debugging ViewModel sharing - val instanceId = System.identityHashCode(this) - - // Platform adapters - private val publisher = AndroidChessPublisher(account) - private val fetcher = AndroidRelayFetcher(account) - private val metadataProvider = AndroidMetadataProvider() - private val dismissedStorage = ChessDismissedGamesStore(chessDismissedGamesData) - // Shared business logic (creates its own ChessLobbyState internally) private val logic = ChessLobbyLogic( - userPubkey = account.userProfile().pubkeyHex, + userPubkey = userPubkey, publisher = publisher, fetcher = fetcher, metadataProvider = metadataProvider, - scope = viewModelScope, - pollingConfig = ChessPollingDefaults.android, + scope = scope ?: viewModelScope, + pollingConfig = pollingConfig, dismissedStorage = dismissedStorage, ) @@ -99,7 +90,7 @@ class ChessViewModelNew( // ============================================ init { - Log.d("chessdebug") { "[AndroidVM] init: instanceId=$instanceId, userPubkey=${account.userProfile().pubkeyHex.take(8)}" } + Log.d("chessdebug") { "[ChessVM] init: userPubkey=${userPubkey.take(8)}" } logic.startPolling() } @@ -143,10 +134,10 @@ class ChessViewModelNew( if (event.kind != JesterProtocol.KIND) return val jesterEvent = event.toJesterEvent() ?: run { - Log.d("chessdebug") { "[AndroidVM] handleIncomingEvent: failed to parse kind ${event.kind} event ${event.id.take(8)} as JesterEvent" } + Log.d("chessdebug") { "[ChessVM] handleIncomingEvent: failed to parse kind ${event.kind} event ${event.id.take(8)} as JesterEvent" } return } - Log.d("chessdebug") { "[AndroidVM] handleIncomingEvent: id=${event.id.take(8)}, pubkey=${event.pubKey.take(8)}, isStart=${jesterEvent.isStartEvent()}, isMove=${jesterEvent.isMoveEvent()}" } + Log.d("chessdebug") { "[ChessVM] handleIncomingEvent: id=${event.id.take(8)}, pubkey=${event.pubKey.take(8)}, isStart=${jesterEvent.isStartEvent()}, isMove=${jesterEvent.isMoveEvent()}" } logic.handleIncomingEvent(jesterEvent) } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt index 348b47a8a6..271312a224 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt @@ -65,8 +65,11 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.cache.UserMetadataCache import com.vitorpamplona.amethyst.commons.nip64Chess.ChessChallenge import com.vitorpamplona.amethyst.commons.nip64Chess.ChessConfig +import com.vitorpamplona.amethyst.commons.nip64Chess.ChessPollingDefaults +import com.vitorpamplona.amethyst.commons.nip64Chess.ChessViewModel import com.vitorpamplona.amethyst.commons.nip64Chess.CompletedGame import com.vitorpamplona.amethyst.commons.nip64Chess.PublicGame +import com.vitorpamplona.amethyst.commons.nip64Chess.desktopChessDismissedGamesStore import com.vitorpamplona.amethyst.commons.nip64Chess.ui.ActiveGameCard import com.vitorpamplona.amethyst.commons.nip64Chess.ui.ChallengeCard import com.vitorpamplona.amethyst.commons.nip64Chess.ui.ChessBroadcastBanner @@ -100,9 +103,18 @@ fun ChessScreen( compactMode: Boolean = false, ) { val scope = rememberCoroutineScope() + val userMetadataCache = remember(account.pubKeyHex) { UserMetadataCache() } val viewModel = remember(account.pubKeyHex) { - DesktopChessViewModelNew(account, relayManager, scope) + ChessViewModel( + userPubkey = account.pubKeyHex, + publisher = DesktopChessPublisher(account, relayManager), + fetcher = DesktopRelayFetcher(relayManager, account.pubKeyHex), + metadataProvider = DesktopMetadataProvider(userMetadataCache), + pollingConfig = ChessPollingDefaults.desktop, + dismissedStorage = desktopChessDismissedGamesStore(), + scope = scope, + ) } val connectedRelays by relayManager.connectedRelays.collectAsState() val broadcastStatus by viewModel.broadcastStatus.collectAsState() @@ -151,7 +163,7 @@ fun ChessScreen( } // Subscribe to user metadata for pubkeys that need it - val pubkeysNeeded by viewModel.userMetadataCache.pubkeysNeeded.collectAsState() + val pubkeysNeeded by userMetadataCache.pubkeysNeeded.collectAsState() rememberSubscription(connectedRelays, pubkeysNeeded, relayManager = relayManager) { if (connectedRelays.isNotEmpty() && pubkeysNeeded.isNotEmpty()) { createMetadataListSubscription( @@ -171,7 +183,7 @@ fun ChessScreen( val publicGames by viewModel.publicGames.collectAsState() val completedGames by viewModel.completedGames.collectAsState() // Observe metadata changes to trigger recomposition - val userMetadata by viewModel.userMetadataCache.metadata.collectAsState() + val userMetadata by userMetadataCache.metadata.collectAsState() val selectedGameId by viewModel.selectedGameId.collectAsState() val error by viewModel.error.collectAsState() val isRefreshing by viewModel.isRefreshing.collectAsState() @@ -289,8 +301,8 @@ fun ChessScreen( DesktopChessGameLayout( gameState = gameState, - opponentName = viewModel.userMetadataCache.getDisplayName(gameState.opponentPubkey), - opponentPicture = viewModel.userMetadataCache.getPictureUrl(gameState.opponentPubkey), + opponentName = userMetadataCache.getDisplayName(gameState.opponentPubkey), + opponentPicture = userMetadataCache.getPictureUrl(gameState.opponentPubkey), onMoveMade = { from, to, _ -> viewModel.publishMove(gameState.startEventId, from, to) }, @@ -306,12 +318,12 @@ fun ChessScreen( null }, compactMode = compactMode, - whiteName = viewModel.userMetadataCache.getDisplayName(whitePubkey), + whiteName = userMetadataCache.getDisplayName(whitePubkey), whiteHex = whitePubkey, - whiteAvatarUrl = viewModel.userMetadataCache.getPictureUrl(whitePubkey), - blackName = viewModel.userMetadataCache.getDisplayName(blackPubkey), + whiteAvatarUrl = userMetadataCache.getPictureUrl(whitePubkey), + blackName = userMetadataCache.getDisplayName(blackPubkey), blackHex = blackPubkey, - blackAvatarUrl = viewModel.userMetadataCache.getPictureUrl(blackPubkey), + blackAvatarUrl = userMetadataCache.getPictureUrl(blackPubkey), ) } } else { @@ -338,7 +350,7 @@ fun ChessScreen( publicGames = publicGames, completedGames = completedGames, userPubkey = account.pubKeyHex, - metadataCache = viewModel.userMetadataCache, + metadataCache = userMetadataCache, onAcceptChallenge = { viewModel.acceptChallenge(it) }, onOpenOwnChallenge = { viewModel.openOwnChallenge(it) }, onWatchGame = { viewModel.loadGameAsSpectator(it) }, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/DesktopChessViewModelNew.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/DesktopChessViewModelNew.kt deleted file mode 100644 index 3a5123f8b7..0000000000 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/DesktopChessViewModelNew.kt +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.desktop.chess - -import com.vitorpamplona.amethyst.commons.model.cache.UserMetadataCache -import com.vitorpamplona.amethyst.commons.nip64Chess.ChessBroadcastStatus -import com.vitorpamplona.amethyst.commons.nip64Chess.ChessChallenge -import com.vitorpamplona.amethyst.commons.nip64Chess.ChessLobbyLogic -import com.vitorpamplona.amethyst.commons.nip64Chess.ChessPollingDefaults -import com.vitorpamplona.amethyst.commons.nip64Chess.ChessSyncStatus -import com.vitorpamplona.amethyst.commons.nip64Chess.CompletedGame -import com.vitorpamplona.amethyst.commons.nip64Chess.PublicGame -import com.vitorpamplona.amethyst.commons.nip64Chess.desktopChessDismissedGamesStore -import com.vitorpamplona.amethyst.desktop.account.AccountState -import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip64Chess.Color -import com.vitorpamplona.quartz.nip64Chess.LiveChessGameState -import com.vitorpamplona.quartz.nip64Chess.jester.JesterProtocol -import com.vitorpamplona.quartz.nip64Chess.jester.toJesterEvent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.StateFlow - -/** - * Slim Desktop ViewModel for chess (~120 lines). - * - * Delegates all business logic to ChessLobbyLogic. - * Only handles Desktop-specific concerns: - * - Platform adapter creation - * - State exposure to Compose Desktop UI - * - UserMetadataCache for profile display - */ -class DesktopChessViewModelNew( - private val account: AccountState.LoggedIn, - private val relayManager: DesktopRelayConnectionManager, - private val scope: CoroutineScope, -) { - // Desktop-specific metadata cache - val userMetadataCache = UserMetadataCache() - - // Platform adapters - private val publisher = DesktopChessPublisher(account, relayManager) - private val fetcher = DesktopRelayFetcher(relayManager, account.pubKeyHex) - private val metadataProvider = DesktopMetadataProvider(userMetadataCache) - private val dismissedStorage = desktopChessDismissedGamesStore() - - // Shared business logic (creates its own ChessLobbyState internally) - private val logic = - ChessLobbyLogic( - userPubkey = account.pubKeyHex, - publisher = publisher, - fetcher = fetcher, - metadataProvider = metadataProvider, - scope = scope, - pollingConfig = ChessPollingDefaults.desktop, - dismissedStorage = dismissedStorage, - ) - - // ============================================ - // State exposure (delegated from ChessLobbyLogic.state) - // ============================================ - - val activeGames: StateFlow<Map<String, LiveChessGameState>> = logic.state.activeGames - val spectatingGames: StateFlow<Map<String, LiveChessGameState>> = logic.state.spectatingGames - val challenges: StateFlow<List<ChessChallenge>> = logic.state.challenges - val publicGames: StateFlow<List<PublicGame>> = logic.state.publicGames - val completedGames: StateFlow<List<CompletedGame>> = logic.state.completedGames - val broadcastStatus: StateFlow<ChessBroadcastStatus> = logic.state.broadcastStatus - val error: StateFlow<String?> = logic.state.error - val selectedGameId: StateFlow<String?> = logic.state.selectedGameId - val isRefreshing: StateFlow<Boolean> = logic.state.isRefreshing - val syncStatus: StateFlow<ChessSyncStatus> = logic.state.syncStatus - val stateVersion: StateFlow<Long> = logic.state.stateVersion - - /** Badge count (incoming challenges + your turn games) - computed property */ - val badgeCount: Int get() = logic.state.badgeCount - - // ============================================ - // Lifecycle - // ============================================ - - init { - logic.startPolling() - } - - fun startPolling() = logic.startPolling() - - fun stopPolling() = logic.stopPolling() - - fun forceRefresh() = logic.forceRefresh() - - /** - * Ensure a game ID is being polled for updates. - * Call this when viewing a game. - */ - fun ensureGamePolling(gameId: String) = logic.ensureGamePolling(gameId) - - /** - * Set focused game mode - only poll this specific game. - * Call this when viewing a game to avoid refreshing unrelated games. - */ - fun setFocusedGame(gameId: String) = logic.setFocusedGame(gameId) - - /** - * Clear focused game mode - return to lobby mode (poll all games). - * Call this when returning to the lobby view. - */ - fun clearFocusedGame() = logic.clearFocusedGame() - - // ============================================ - // Incoming event routing (from relay subscriptions) - // ============================================ - - fun handleIncomingEvent(event: Event) { - if (event.kind != JesterProtocol.KIND) return - val jesterEvent = event.toJesterEvent() ?: return - logic.handleIncomingEvent(jesterEvent) - } - - // ============================================ - // Challenge operations - // ============================================ - - fun createChallenge( - opponentPubkey: String? = null, - playerColor: Color = Color.WHITE, - timeControl: String? = null, - ) = logic.createChallenge(opponentPubkey, playerColor, timeControl) - - fun acceptChallenge(challenge: ChessChallenge) = logic.acceptChallenge(challenge) - - fun openOwnChallenge(challenge: ChessChallenge) = logic.openOwnChallenge(challenge) - - // ============================================ - // Game operations - // ============================================ - - fun selectGame(gameId: String?) = logic.selectGame(gameId) - - fun publishMove( - gameId: String, - from: String, - to: String, - ) = logic.publishMove(gameId, from, to) - - fun resign(gameId: String) = logic.resign(gameId) - - fun claimAbandonmentVictory(gameId: String) = logic.claimAbandonmentVictory(gameId) - - fun dismissGame(gameId: String) = logic.dismissGame(gameId) - - fun dismissCompletedGame(gameId: String) = logic.dismissCompletedGame(gameId) - - fun dismissAllCompletedGames() = logic.dismissAllCompletedGames() - - // ============================================ - // Spectator operations - // ============================================ - - fun loadGame(gameId: String) = logic.loadGame(gameId) - - fun loadGameAsSpectator(gameId: String) = logic.loadGameAsSpectator(gameId) - - fun stopSpectating(gameId: String) = logic.stopSpectating(gameId) - - fun removeGame(gameId: String) = logic.removeGame(gameId) - - // ============================================ - // Utility - // ============================================ - - fun clearError() = logic.clearError() - - fun getGameState(gameId: String): LiveChessGameState? = logic.state.getGameState(gameId) - - /** Check if a game was accepted (prevents loading as spectator during race) */ - fun wasAccepted(gameId: String): Boolean = logic.state.wasAccepted(gameId) - - /** Helper for derived challenge lists */ - fun incomingChallenges(): List<ChessChallenge> = logic.state.incomingChallenges() - - fun outgoingChallenges(): List<ChessChallenge> = logic.state.outgoingChallenges() - - fun openChallenges(): List<ChessChallenge> = logic.state.openChallenges() -} From 9512fd886f1d61c019e535db8ccd124e2b772f49 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sun, 27 Sep 2026 03:28:08 +0000 Subject: [PATCH 38/60] refactor: move the unblocked amethyst/ui files to commons and commonsUI A transitive-blocker sweep (imports plus same-package references) found 96 files under amethyst/ui that no longer depend on Android or on app-only code. This moves 68 of them, deletes 3 app copies of code that commons already had, and leaves 25 that are Android-only by nature. - To commons: the chatroom-list and relay-group filters, the *LastRead routes, composer state (NewMessageTagger, DraftTagState, IExpiration, PreviewState, SplitConversor), FilterByListParams and the small feed states, RelayFeedViewModel, UserExternalIdentitiesViewModel, TorDialogViewModel, the QR structured-append accumulator, PitchShifter and others. - To commonsUI: InformationDialog, UrlPreviewCard, the mention input transformation, the nav wrappers (ObservableNav, TwoPaneNav, ShareToDMNav), SettingsCatalog, KeyboardState and feature composables under their feature ui/ packages. - Deleted app copies: ui/note/PubKeyFormatter, ui/note/ZapFormatter and the EqualImmutableLists shim; callers use the commons originals. - commonMain fixes the moves needed: String.format, codePointAt, Integer.parseInt, javaClass and a Class<T> ViewModel factory replaced by their KMP equivalents. - Eleven pure unit tests moved with their code. The per-file table, the files kept in the app and why, and what the sweep found outside ui/ are in commons/plans/2026-08-30-commons-migration-sweep.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkG4KTqmnprUK9BAcwAyc8 --- .claude/skills/feed-patterns/SKILL.md | 4 +- .../references/feed-filter-composition.md | 2 +- .../references/abstraction-examples.md | 4 +- ...entionPreservingInputTransformationTest.kt | 2 +- .../loggedIn/settings/LegalSettingsSection.kt | 1 + .../baselineProfiles/baseline-prof.txt | 26 ++-- .../vitorpamplona/amethyst/model/Account.kt | 2 +- .../amethyst/model/AccountConcordActions.kt | 2 +- .../amethyst/model/AccountSyncedSettings.kt | 2 +- .../NotificationReplyReceiver.kt | 2 +- .../notifications/RelayPurposeSummary.kt | 2 +- .../renderers/NwcPaymentNotifier.kt | 2 +- .../renderers/ZapNotification.kt | 2 +- .../RelaySubscriptionsCoordinator.kt | 2 +- .../amethyst/ui/actions/EditPostView.kt | 2 + .../amethyst/ui/actions/EditPostViewModel.kt | 4 +- .../amethyst/ui/actions/NewMediaModel.kt | 2 +- .../amethyst/ui/actions/NewMediaView.kt | 1 + .../actions/UrlUserTagOutputTransformation.kt | 1 + .../mediaServers/AllMediaServersLIstView.kt | 1 + .../mediaServers/BlossomServersViewModel.kt | 2 + .../ui/actions/uploads/RecordVoiceButton.kt | 2 + .../uploads/VoiceAnonymizationController.kt | 1 + .../uploads/VoiceAnonymizationSection.kt | 1 + .../ui/actions/uploads/VoiceAnonymizer.kt | 2 + .../ui/actions/uploads/VoiceMessagePreview.kt | 1 + .../ui/actions/uploads/VoicePresetSelector.kt | 1 + .../amethyst/ui/components/LoadUrlPreview.kt | 1 + .../amethyst/ui/components/RichTextViewer.kt | 2 +- .../ui/components/ZoomableContentView.kt | 2 +- .../components/toasts/DisplayErrorMessages.kt | 2 +- .../amethyst/ui/navigation/AppNavigation.kt | 6 +- .../ui/navigation/bottombars/AppBottomBar.kt | 3 + .../bottombars/AppNavigationRail.kt | 1 + .../drawer/AccountSwitchBottomSheet.kt | 2 +- .../ui/navigation/navs/RememberNavs.kt | 1 + .../ui/navigation/routes/RouteMaker.kt | 1 + .../amethyst/ui/note/Bolt12ZapGallery.kt | 1 + .../amethyst/ui/note/MultiSetCompose.kt | 1 + .../amethyst/ui/note/NutzapGallery.kt | 1 + .../amethyst/ui/note/NutzapUserSetCompose.kt | 1 + .../amethyst/ui/note/OnchainZapGallery.kt | 1 + .../amethyst/ui/note/PubKeyFormatter.kt | 33 ----- .../amethyst/ui/note/ReactionsRow.kt | 3 +- .../amethyst/ui/note/RelayCompose.kt | 2 +- .../amethyst/ui/note/UpdateZapAmountDialog.kt | 1 + .../amethyst/ui/note/ZapCustomDialog.kt | 1 + .../amethyst/ui/note/ZapFormatter.kt | 83 ----------- .../amethyst/ui/note/ZapPollNote.kt | 1 + .../expiration/ExpirationDatePicker.kt | 1 + .../creators/messagefield/MessageField.kt | 2 +- .../note/creators/previews/DisplayPreviews.kt | 1 + .../ui/note/creators/previews/PreviewUrl.kt | 2 +- .../note/creators/zapsplits/ForwardZapTo.kt | 2 +- .../ui/note/elements/DisplayReward.kt | 2 +- .../nip22Comments/CommentPostViewModel.kt | 14 +- .../note/nip22Comments/DisplayExternalId.kt | 2 +- .../nip22Comments/GenericCommentPostScreen.kt | 3 +- .../amethyst/ui/note/types/Chat.kt | 1 + .../amethyst/ui/note/types/Classifieds.kt | 2 +- .../amethyst/ui/note/types/CommunityHeader.kt | 2 +- .../amethyst/ui/note/types/Goal.kt | 2 +- .../amethyst/ui/note/types/LiveActivity.kt | 6 +- .../amethyst/ui/note/types/MeetingSpace.kt | 6 +- .../amethyst/ui/note/types/MusicPlaylist.kt | 1 + .../amethyst/ui/note/types/MusicTrack.kt | 1 + .../amethyst/ui/note/types/Nutzap.kt | 2 +- .../amethyst/ui/screen/UserFeedView.kt | 1 + .../amethyst/ui/screen/UserFeedViewModel.kt | 3 +- .../ui/screen/loggedIn/AccountViewModel.kt | 7 +- .../articles/dal/ArticlesFeedFilter.kt | 2 +- .../loggedIn/badges/dal/BadgesFeedFilter.kt | 2 +- .../loggedIn/badges/post/NewBadgeDialog.kt | 2 +- .../loggedIn/badges/post/NewBadgeModel.kt | 2 +- .../loggedIn/buzz/AgentWorkBoardScreen.kt | 3 + .../loggedIn/buzz/AgentWorkBoardViewModel.kt | 2 + .../loggedIn/buzz/BuzzForumPostScreen.kt | 2 +- .../buzz/ForumReplyNewMessageViewModel.kt | 2 +- .../loggedIn/calendars/CalendarsScreen.kt | 1 + .../loggedIn/calendars/CalendarsTopBar.kt | 1 + .../loggedIn/calendars/CalendarsViewModel.kt | 1 + .../dal/CalendarAppointmentsFeedFilter.kt | 2 +- .../dal/CalendarCollectionsFeedFilter.kt | 2 +- .../chats/cordnGroup/CordnComposer.kt | 2 +- .../loggedIn/chats/feed/ChatFeedView.kt | 2 +- .../loggedIn/chats/feed/LoadingReplyNote.kt | 1 + .../chats/feed/types/RenderChatZap.kt | 2 +- .../chats/feed/types/RenderRegularTextNote.kt | 2 +- .../chats/marmotGroup/MarmotGroupChatView.kt | 3 +- .../marmotGroup/MarmotGroupListScreen.kt | 1 + .../loggedIn/chats/privateDM/ChatroomView.kt | 2 +- .../privateDM/send/ChatNewMessageViewModel.kt | 12 +- .../chats/privateDM/send/NewGroupDMScreen.kt | 2 +- .../send/PrivateMessageEditFieldRow.kt | 4 +- .../concord/ConcordChannelScreen.kt | 5 +- .../publicChannels/concord/ConcordUnread.kt | 1 + .../header/LiveStreamTopZappers.kt | 2 +- .../header/LongLiveActivityChannelHeader.kt | 2 +- .../header/ShortLiveActivityChannelHeader.kt | 4 +- .../relayGroup/RelayGroupChannelView.kt | 3 +- .../relayGroup/RelayGroupDiscoveryScreen.kt | 2 +- .../relayGroup/RelayGroupUnread.kt | 1 + .../dal/RelayGroupDiscoveryFeedFilter.kt | 1 + .../RelayGroupOpenThreadsSubscription.kt | 2 + .../RelayGroupsDiscoverySubAssembler.kt | 1 + .../send/ChannelNewMessageViewModel.kt | 8 +- .../chats/publicChannels/send/EditFieldRow.kt | 4 +- .../chats/rooms/ChatroomHeaderCompose.kt | 10 +- .../chats/rooms/MarkChatRoomsAsRead.kt | 10 +- .../screen/loggedIn/chats/rooms/RowUnread.kt | 10 +- .../rooms/dal/ChatroomListKnownFeedFilter.kt | 2 + .../ChatroomListNip04HistorySubAssembler.kt | 2 + .../ChatroomListNip04SubAssembler.kt | 2 + .../FollowingEphemeralChatSubAssembler.kt | 1 + .../FollowingPublicChatSubAssembler.kt | 2 + .../chats/rooms/feed/ChatroomListFeedView.kt | 2 +- .../chats/rooms/twopane/MessagesTwoPane.kt | 1 + .../loggedIn/chats/share/ShareToDMScreen.kt | 1 + .../chats/utils/ChatFileUploadState.kt | 2 +- .../loggedIn/chess/dal/ChessFeedFilter.kt | 2 +- .../communities/dal/CommunityFeedFilter.kt | 2 + .../dal/CommunityModerationFeedFilter.kt | 2 + .../list/dal/CommunitiesFeedFilter.kt | 2 +- .../newCommunity/NewCommunityModel.kt | 2 +- .../newCommunity/NewCommunityScreen.kt | 2 +- .../DiscoverLongFormFeedFilter.kt | 2 +- .../nip23LongForm/LongFormPostScreen.kt | 4 +- .../nip23LongForm/LongFormPostViewModel.kt | 12 +- .../nip28Chats/DiscoverChatFeedFilter.kt | 2 +- .../RenderPublicChatChannelThumb.kt | 2 +- .../DiscoverFollowSetsFeedFilter.kt | 2 +- .../DiscoverLiveFeedFilter.kt | 2 +- .../nip53LiveActivities/LiveActivityCard.kt | 10 +- .../nip72Communities/CommunityCard.kt | 2 +- .../DiscoverCommunityFeedFilter.kt | 2 +- .../nip90DVMs/DiscoverNIP89FeedFilter.kt | 2 +- .../nip99Classifieds/ClassifiedsThumb.kt | 2 +- .../DiscoverMarketplaceFeedFilter.kt | 2 +- .../nip99Classifieds/NewProductScreen.kt | 2 +- .../nip99Classifieds/NewProductViewModel.kt | 14 +- .../discover/nip99Classifieds/SellProduct.kt | 2 +- .../dal/DvmContentDiscoveryResponseFilter.kt | 2 +- .../browse/dal/BrowseEmojiSetsFeedFilter.kt | 2 +- .../FollowPackFeedConversationsFeedFilter.kt | 2 +- .../dal/FollowPackFeedNewThreadFeedFilter.kt | 2 +- .../list/dal/FollowPacksFeedFilter.kt | 2 +- .../geocaches/dal/GeocacheHuntsFeedFilter.kt | 2 +- .../geocaches/dal/GeocacheMineFeedFilter.kt | 1 + .../geocaches/dal/GeocachesFeedFilter.kt | 3 +- .../geocaches/dal/GeocachesHereFeedFilter.kt | 1 + .../dal/GitRepositoriesFeedFilter.kt | 2 +- .../HashtagFeedFilterSubAssembler.kt | 1 + .../highlights/NewHighlightPostViewModel.kt | 2 +- .../highlights/dal/HighlightsFeedFilter.kt | 2 +- .../loggedIn/home/ShortNotePostScreen.kt | 2 +- .../loggedIn/home/ShortNotePostViewModel.kt | 16 +-- .../loggedIn/home/VoiceReplyViewModel.kt | 2 +- .../home/dal/HomeConversationsFeedFilter.kt | 2 +- .../loggedIn/home/dal/HomeLiveFilter.kt | 2 +- .../home/dal/HomeNewThreadFeedFilter.kt | 2 +- .../livestreams/dal/LiveStreamsFeedFilter.kt | 2 +- .../loggedIn/longs/dal/LongsFeedFilter.kt | 4 +- .../loggedIn/music/NewMusicPlaylistScreen.kt | 2 +- .../music/NewMusicPlaylistViewModel.kt | 2 +- .../loggedIn/music/NewMusicTrackScreen.kt | 2 +- .../loggedIn/music/NewMusicTrackViewModel.kt | 2 +- .../music/dal/MusicPlaylistsFeedFilter.kt | 2 +- .../music/dal/MusicTracksFeedFilter.kt | 2 +- .../napplets/ConnectedAppDetailScreen.kt | 2 +- .../screen/loggedIn/nests/NestsFeedLoaded.kt | 8 +- .../loggedIn/nests/dal/NestsFeedFilter.kt | 2 +- .../nests/room/chat/NestEditFieldRow.kt | 4 +- .../room/chat/NestNewMessageViewModel.kt | 8 +- .../nests/room/stage/SpeakerZapOverlay.kt | 2 +- .../notifications/CardFeedContentState.kt | 1 + .../loggedIn/notifications/CardFeedView.kt | 2 +- .../notifications/EqualImmutableLists.kt | 31 ---- .../notifications/NotificationSummaryState.kt | 2 +- .../chart/AmountValueFormatter.kt | 4 +- .../dal/NotificationFeedFilter.kt | 2 +- .../publicMessages/NewPublicMessageScreen.kt | 4 +- .../NewPublicMessageViewModel.kt | 14 +- .../pictures/dal/PictureFeedFilter.kt | 2 +- .../loggedIn/podcasts/PodcastTopSupporters.kt | 2 +- .../authoring/EditPodcastShowScreen.kt | 2 +- .../authoring/EditPodcastShowViewModel.kt | 2 +- .../authoring/NewPodcastEpisodeScreen.kt | 2 +- .../authoring/NewPodcastEpisodeViewModel.kt | 2 +- .../authoring/NewPodcastTrailerScreen.kt | 2 +- .../authoring/NewPodcastTrailerViewModel.kt | 2 +- .../podcasts/dal/PodcastEpisodesFeedFilter.kt | 2 +- .../podcasts/dal/PodcastsFeedFilter.kt | 2 +- .../polls/dal/ClosedPollsFeedFilter.kt | 2 +- .../loggedIn/polls/dal/OpenPollsFeedFilter.kt | 2 +- .../loggedIn/polls/dal/PollsFeedFilter.kt | 2 +- .../polls/results/PollResultsScreen.kt | 1 + .../products/dal/ProductsFeedFilter.kt | 2 +- .../screen/loggedIn/profile/ProfileScreen.kt | 2 +- .../apps/dal/UserProfileAppsFeedFilter.kt | 2 +- .../dal/UserProfileGalleryFeedFilter.kt | 2 +- .../profile/header/DrawAdditionalInfo.kt | 2 +- .../loggedIn/profile/header/ProfileHeader.kt | 2 +- .../profile/payment/SendPaymentContent.kt | 2 +- .../profile/payment/SendPaymentScreen.kt | 2 +- .../loggedIn/profile/relays/RelayFeedView.kt | 2 + .../loggedIn/profile/relays/TabRelays.kt | 1 + .../loggedIn/profile/zaps/ZapNoteCompose.kt | 2 +- .../loggedIn/profile/zaps/ZapTabHeader.kt | 2 +- .../publicChats/dal/PublicChatsFeedFilter.kt | 2 +- .../loggedIn/qrcode/scanner/BarcodeDecoder.kt | 3 + .../qrcode/scanner/QrFrameAnalyzer.kt | 2 + .../loggedIn/qrcode/scanner/QrImageImport.kt | 1 + .../qrcode/scanner/QrScannerDialog.kt | 1 + .../qrcode/scanner/QrScannerOverlay.kt | 3 + .../loggedIn/qrcode/scanner/QrScannerState.kt | 3 + .../ActiveSubscriptionsScreen.kt | 2 +- .../loggedIn/settings/AllSettingsScreen.kt | 4 + .../loggedIn/settings/HiddenWordsScreen.kt | 1 + .../loggedIn/settings/NIP47SetupScreen.kt | 1 + .../settings/NotificationSettingsScreen.kt | 2 +- .../loggedIn/settings/SecurityListsCommon.kt | 2 +- .../settings/SettingsCatalogBuilder.kt | 3 + .../loggedIn/settings/StringFeedView.kt | 1 + .../loggedIn/settings/StringFeedViewModel.kt | 3 +- .../settings/cordn/CordnBackupScreen.kt | 2 + .../settings/cordn/CordnCoordinatorsScreen.kt | 1 + .../settings/cordn/CordnKeyPackagesScreen.kt | 1 + .../settings/cordn/CordnLinkScreen.kt | 1 + .../settings/cordn/CordnMigrateScreen.kt | 2 + .../settings/nip46/Nip46SignerScreen.kt | 1 + .../loggedIn/shorts/dal/ShortsFeedFilter.kt | 4 +- .../dal/SoftwareAppsFeedFilter.kt | 2 +- .../loggedIn/threadview/ThreadFeedView.kt | 2 +- .../ui/screen/loggedIn/url/UrlScreen.kt | 2 +- .../loggedIn/video/dal/VideoFeedFilter.kt | 3 +- .../video/hls/HlsPublishOrchestrator.kt | 1 + .../hls/HlsPublishOrchestratorFactory.kt | 1 + .../loggedIn/video/hls/NewHlsVideoScreen.kt | 1 + .../video/hls/NewHlsVideoViewModel.kt | 1 + .../loggedIn/wallet/OnchainZapSendDialog.kt | 2 +- .../loggedIn/wallet/ReloadMintScreen.kt | 2 +- .../ui/screen/loggedIn/wallet/WalletScreen.kt | 1 + .../wallet/WalletTransactionsScreen.kt | 1 + .../wizard/CashuWalletWizardViewModel.kt | 1 + .../workouts/dal/WorkoutFeedFilter.kt | 2 +- .../ui/screen/loggedOff/login/LoginScreen.kt | 1 + .../screen/loggedOff/login/LoginViewModel.kt | 1 + .../screen/loggedOff/signup/SignUpScreen.kt | 2 +- .../loggedOff/signup/SignUpViewModel.kt | 2 +- .../amethyst/ui/tor/TorSettingsDialog.kt | 1 + .../loggedIn/settings/LegalSettingsSection.kt | 3 + .../calendar/CalendarsViewModelTest.kt | 2 +- .../uploads/hls/HlsPublishOrchestratorTest.kt | 2 +- .../qrcode/scanner/QrScannerStateTest.kt | 3 + .../login/LoginViewModelKeyKindTest.kt | 1 + commons/ARCHITECTURE.md | 4 +- .../2026-08-30-commons-migration-sweep.md | 133 ++++++++++++++++++ commons/plans/README.md | 5 +- .../amethyst/commons/audio}/PitchShifter.kt | 3 +- .../commons/feeds}/FilterByListParams.kt | 2 +- .../commons/feeds}/StringFeedState.kt | 2 +- .../commons/feeds}/SupportedContent.kt | 2 +- .../amethyst/commons/feeds}/UserFeedState.kt | 2 +- .../commons/model}/ConcordLastRead.kt | 2 +- .../commons/model}/MarmotGroupLastRead.kt | 2 +- .../commons/model}/RelayGroupLastRead.kt | 2 +- .../amethyst/commons/model}/buzz/AgentWork.kt | 6 +- .../model/chats}/ConcordServerRoomNote.kt | 2 +- .../model/chats}/RelayGroupServerRoomNote.kt | 2 +- .../commons/model/composer}/DraftTagState.kt | 2 +- .../commons/model/composer}/IExpiration.kt | 2 +- .../model/composer}/NewMessageTagger.kt | 2 +- .../commons/model/composer}/SplitConversor.kt | 3 +- .../model/navigation}/RouteTextArgs.kt | 13 +- .../navigation}/ShareToDMRouteRewriter.kt | 4 +- .../GroupDiscoveryConstraintResolver.kt | 3 +- .../TransactionRowLabels.kt | 2 +- .../nip72Communities}/CommunityRulesLookup.kt | 2 +- .../commons/model/nip92IMeta}/ImetaContent.kt | 2 +- .../commons/music}/MusicFormatting.kt | 6 +- .../amethyst/commons/qrcode}/ScanResult.kt | 2 +- .../qrcode}/StructuredAppendAccumulator.kt | 2 +- .../RelayGroupOpenThreadsFilterAssembler.kt | 3 +- .../relayGroup}/RelayGroupsDiscoveryFilter.kt | 11 +- .../FilterFollowingEphemeralChats.kt | 2 +- .../chatrooms}/FilterFollowingPublicChats.kt | 2 +- .../FilterLastMessageFollowingPublicChats.kt | 2 +- .../chatrooms}/FilterNip04DMsFromMe.kt | 2 +- .../chatrooms}/FilterNip04DMsToMe.kt | 2 +- .../geocaches}/GeocacheListingKinds.kt | 2 +- .../hashtag}/FilterPostsByHashtags.kt | 2 +- .../polls/results/RelayPollResponseLoader.kt | 2 +- .../service/upload}/HlsPublishState.kt | 2 +- .../service/upload}/MediaUploadTracker.kt | 2 +- .../upload}/SuspendableConfirmation.kt | 2 +- .../commons}/tor/TorDialogViewModel.kt | 12 +- .../commons/util}/DebouncedPublisher.kt | 2 +- .../commons/viewmodels}/RelayFeedViewModel.kt | 5 +- .../UserExternalIdentitiesViewModel.kt | 10 +- .../commons/cashu}/CashuWalletDiscovery.kt | 2 +- .../mediaServers/MediaServerHealth.kt | 2 +- .../commons/model/chats}/HistoryDateFormat.kt | 2 +- .../commons/model/composer}/PreviewState.kt | 2 +- .../amethyst/commons/util}/TimeFormatUtils.kt | 2 +- .../amethyst/commons/util/ZapFormatter.kt | 12 +- .../commons/util}/ZapFormatterNoDecimals.kt | 2 +- .../commons/audio}/PitchShifterTest.kt | 2 +- .../commons/feeds}/SupportedContentTest.kt | 2 +- .../composer}/NewMessageTaggerKeyParseTest.kt | 3 +- .../model}/navigation/RouteTextArgsTest.kt | 6 +- .../navigation/ShareToDMRouteRewriterTest.kt | 4 +- .../RelayGroupDiscoveryConstraintTest.kt | 4 +- .../TransactionRowLabelsTest.kt | 3 +- .../CommunityRulesLookupTest.kt | 2 +- .../StructuredAppendAccumulatorTest.kt | 2 +- .../commons/util}/DebouncedPublisherTest.kt | 2 +- .../account/ui}/login/LoginErrorManager.kt | 2 +- .../amethyst/commons/audio}/VoicePreset.kt | 2 +- .../commons/clink/ui}/ClinkBudgetDialog.kt | 2 +- .../amethyst/commons/cordn/ui}/BusyLabel.kt | 2 +- .../commons/cordn/ui}/SettingsFormBlock.kt | 2 +- .../nip46RemoteSigner/ui}/Nip46ActivityUi.kt | 2 +- .../nip52Calendar/ui}/CalendarsViewMode.kt | 2 +- .../ui}/CommunityRulesViolationBanner.kt | 2 +- .../commons/relays/ui}/SubPurposeLabels.kt | 2 +- .../upload/ui}/StrippingFailureDialog.kt | 6 +- .../commons/ui/insets}/KeyboardState.kt | 3 +- .../bottombars/TabReselectCoordinator.kt | 2 +- .../ui/navigation/navs/ObservableNav.kt | 3 +- .../ui/navigation/navs}/ShareToDMNav.kt | 4 +- .../commons/ui/navigation/navs}/TwoPaneNav.kt | 3 +- .../commons/ui}/settings/SettingsCatalog.kt | 2 +- .../MentionPreservingInputTransformation.kt | 6 +- .../commons/audio}/RecordingIndicators.kt | 3 +- .../ui}/StreamingStatusFlags.kt | 2 +- .../ui/components}/InformationDialog.kt | 2 +- .../commons}/ui/components/UrlPreviewCard.kt | 5 +- .../commons}/ui/theme/ColorSchemePreview.kt | 3 +- .../ui}/settings/SettingsCatalogFilterTest.kt | 2 +- 339 files changed, 639 insertions(+), 551 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatter.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/EqualImmutableLists.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio}/PitchShifter.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds}/FilterByListParams.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds}/StringFeedState.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds}/SupportedContent.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds}/UserFeedState.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model}/ConcordLastRead.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model}/MarmotGroupLastRead.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model}/RelayGroupLastRead.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model}/buzz/AgentWork.kt (94%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/chats}/ConcordServerRoomNote.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/chats}/RelayGroupServerRoomNote.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/draftTags => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer}/DraftTagState.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer}/IExpiration.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer}/NewMessageTagger.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer}/SplitConversor.kt (92%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/navigation}/RouteTextArgs.kt (93%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/navigation}/ShareToDMRouteRewriter.kt (92%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/dal/GroupDiscoveryConstraint.kt => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/GroupDiscoveryConstraintResolver.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip47WalletConnect}/TransactionRowLabels.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip72Communities}/CommunityRulesLookup.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip92IMeta}/ImetaContent.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/music}/MusicFormatting.kt (89%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/qrcode}/ScanResult.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/qrcode}/StructuredAppendAccumulator.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup}/RelayGroupOpenThreadsFilterAssembler.kt (94%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup}/RelayGroupsDiscoveryFilter.kt (78%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatrooms}/FilterFollowingEphemeralChats.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatrooms}/FilterFollowingPublicChats.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatrooms}/FilterLastMessageFollowingPublicChats.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatrooms}/FilterNip04DMsFromMe.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatrooms}/FilterNip04DMsToMe.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/geocaches}/GeocacheListingKinds.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/hashtag}/FilterPostsByHashtags.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient}/polls/results/RelayPollResponseLoader.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload}/HlsPublishState.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload}/MediaUploadTracker.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload}/SuspendableConfirmation.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/tor/TorDialogViewModel.kt (90%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util}/DebouncedPublisher.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels}/RelayFeedViewModel.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/identity => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels}/UserExternalIdentitiesViewModel.kt (88%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/wizard => commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu}/CashuWalletDiscovery.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions => commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons}/mediaServers/MediaServerHealth.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed => commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/chats}/HistoryDateFormat.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews => commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/composer}/PreviewState.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads => commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util}/TimeFormatUtils.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note => commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util}/ZapFormatterNoDecimals.kt (98%) rename {amethyst/src/test/java/com/vitorpamplona/amethyst/ui/actions/uploads => commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/audio}/PitchShifterTest.kt (98%) rename {amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal => commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/feeds}/SupportedContentTest.kt (98%) rename {amethyst/src/test/java/com/vitorpamplona/amethyst => commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/composer}/NewMessageTaggerKeyParseTest.kt (98%) rename {amethyst/src/test/java/com/vitorpamplona/amethyst => commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model}/navigation/RouteTextArgsTest.kt (91%) rename {amethyst/src/test/java/com/vitorpamplona/amethyst => commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model}/navigation/ShareToDMRouteRewriterTest.kt (91%) rename {amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/dal => commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups}/RelayGroupDiscoveryConstraintTest.kt (96%) rename {amethyst/src/test/java/com/vitorpamplona/amethyst/ui/wallet => commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip47WalletConnect}/TransactionRowLabelsTest.kt (97%) rename {amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal => commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip72Communities}/CommunityRulesLookupTest.kt (98%) rename {amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner => commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/qrcode}/StructuredAppendAccumulatorTest.kt (98%) rename {amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn => commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/util}/DebouncedPublisherTest.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/account/ui}/login/LoginErrorManager.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio}/VoicePreset.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/clink/ui}/ClinkBudgetDialog.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/cordn/ui}/BusyLabel.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/cordn/ui}/SettingsFormBlock.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46 => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip46RemoteSigner/ui}/Nip46ActivityUi.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip52Calendar/ui}/CalendarsViewMode.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip72ModCommunities/ui}/CommunityRulesViolationBanner.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relays/ui}/SubPurposeLabels.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/ui}/StrippingFailureDialog.kt (91%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/insets}/KeyboardState.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/navigation/bottombars/TabReselectCoordinator.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/ui/navigation/navs/ObservableNav.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/navs}/ShareToDMNav.kt (93%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/navs}/TwoPaneNav.kt (93%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui}/settings/SettingsCatalog.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions => commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/text}/MentionPreservingInputTransformation.kt (94%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads => commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/audio}/RecordingIndicators.kt (98%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities => commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip53LiveActivities/ui}/StreamingStatusFlags.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions => commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/components}/InformationDialog.kt (99%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons}/ui/components/UrlPreviewCard.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons}/ui/theme/ColorSchemePreview.kt (98%) rename {amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn => commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui}/settings/SettingsCatalogFilterTest.kt (98%) diff --git a/.claude/skills/feed-patterns/SKILL.md b/.claude/skills/feed-patterns/SKILL.md index ab8846db84..34acbe1ed5 100644 --- a/.claude/skills/feed-patterns/SKILL.md +++ b/.claude/skills/feed-patterns/SKILL.md @@ -1,6 +1,6 @@ --- name: feed-patterns -description: Feed composition and data-access layer patterns in Amethyst. Use when adding or modifying a feed (home, profile, hashtag, bookmarks, notifications, DMs, communities), working with the shared `FeedFilter` / `AdditiveFeedFilter` / `ChangesFlowFilter` / `FeedContentState` in `commons/.../feeds/`, the Android-only `AdditiveComplexFeedFilter` / `FilterByListParams` in `amethyst/.../ui/dal/`, or extending the `FeedViewModel` family in `commons/.../viewmodels/`. Covers how feeds scan `LocalCache`, react to changes, apply ordering, and render through Compose. +description: Feed composition and data-access layer patterns in Amethyst. Use when adding or modifying a feed (home, profile, hashtag, bookmarks, notifications, DMs, communities), working with the shared `FeedFilter` / `AdditiveFeedFilter` / `ChangesFlowFilter` / `FeedContentState` in `commons/.../feeds/`, `FilterByListParams` in `commons/.../feeds/`, the Android-only `AdditiveComplexFeedFilter` in `amethyst/.../ui/dal/`, or extending the `FeedViewModel` family in `commons/.../viewmodels/`. Covers how feeds scan `LocalCache`, react to changes, apply ordering, and render through Compose. --- # Feed Patterns @@ -101,7 +101,7 @@ Concrete filters (Home, Hashtag, Profile, Bookmark, Notifications, Communities, ## Filter Sharing (Android vs Desktop) - The filter **base classes** (`FeedFilter`, `AdditiveFeedFilter`, `ChangesFlowFilter`) and feed state (`FeedContentState`) are in `commons/.../feeds/` — **shared**. ViewModels are in `commons/.../viewmodels/` — **shared**. -- The **concrete** filters are platform-local: Android's in `amethyst/.../ui/screen/loggedIn/*/dal/`, Desktop's in `desktopApp/.../feeds/`. `amethyst/.../ui/dal/` keeps Android-only helpers (`AdditiveComplexFeedFilter`, `FilterByListParams`, `DefaultFeedOrder`) plus back-compat typealiases. +- The **concrete** filters are platform-local: Android's in `amethyst/.../ui/screen/loggedIn/*/dal/`, Desktop's in `desktopApp/.../feeds/`. `amethyst/.../ui/dal/` keeps Android-only helpers (`AdditiveComplexFeedFilter`, `DefaultFeedOrder`) plus back-compat typealiases; `FilterByListParams` is shared, in `commons/.../feeds/`. - When porting a feed, share the concrete filter only if both platforms need identical inclusion rules. ## Gotchas diff --git a/.claude/skills/feed-patterns/references/feed-filter-composition.md b/.claude/skills/feed-patterns/references/feed-filter-composition.md index 4c00329919..8946e9a105 100644 --- a/.claude/skills/feed-patterns/references/feed-filter-composition.md +++ b/.claude/skills/feed-patterns/references/feed-filter-composition.md @@ -12,7 +12,7 @@ Step-by-step recipe for composing a new feed. Assume the feed shows `Note`s filt | The feed is a simple list that changes frequently (e.g. bookmarks, lists) | `FeedFilter<Note>` + `ListChangeFeedViewModel` | | The feed is a DM thread | `ChatroomFeedViewModel` (already provides filter machinery) | -The bases live in `commons/src/commonMain/.../commons/ui/feeds/`; `AdditiveComplexFeedFilter` and the `FilterByListParams` / `DefaultFeedOrder` helpers in `amethyst/src/main/java/.../ui/dal/`. +The bases live in `commons/src/commonMain/.../commons/ui/feeds/`; `AdditiveComplexFeedFilter` and the `DefaultFeedOrder` helper in `amethyst/src/main/java/.../ui/dal/`; `FilterByListParams` in `commons/src/commonMain/.../commons/feeds/`. ## 2. Write the Filter diff --git a/.claude/skills/kotlin-multiplatform/references/abstraction-examples.md b/.claude/skills/kotlin-multiplatform/references/abstraction-examples.md index e1277dc75c..bbdc2c9bae 100644 --- a/.claude/skills/kotlin-multiplatform/references/abstraction-examples.md +++ b/.claude/skills/kotlin-multiplatform/references/abstraction-examples.md @@ -253,13 +253,13 @@ fun String.toDisplayHexKey(): String { **After:** ```kotlin -// commons/commonMain/formatters/PubKeyFormatter.kt +// commons/src/commonMain/.../commons/util/PubKeyFormatter.kt fun String.toDisplayHexKey(): String { return "${take(8)}:${takeLast(8)}" } // Both apps use it -import com.vitorpamplona.amethyst.commons.formatters.toDisplayHexKey +import com.vitorpamplona.amethyst.commons.util.toDisplayHexKey ``` **Why successful:** diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/MentionPreservingInputTransformationTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/MentionPreservingInputTransformationTest.kt index 227812f324..155ce6a0c4 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/MentionPreservingInputTransformationTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/MentionPreservingInputTransformationTest.kt @@ -26,7 +26,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.text.input.TextFieldBuffer import androidx.compose.foundation.text.input.TextFieldState import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation +import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/LegalSettingsSection.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/LegalSettingsSection.kt index a4ee5ec78a..5ab3d60652 100644 --- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/LegalSettingsSection.kt +++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/LegalSettingsSection.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings import androidx.compose.ui.platform.UriHandler +import com.vitorpamplona.amethyst.commons.ui.settings.SettingsCategory // F-Droid distributes Amethyst as MIT-licensed free software; the build must // not surface links to external (e.g. GitHub-hosted) policy documents. diff --git a/amethyst/src/main/generated/baselineProfiles/baseline-prof.txt b/amethyst/src/main/generated/baselineProfiles/baseline-prof.txt index 016fe51292..90f8c4c78e 100644 --- a/amethyst/src/main/generated/baselineProfiles/baseline-prof.txt +++ b/amethyst/src/main/generated/baselineProfiles/baseline-prof.txt @@ -25005,11 +25005,11 @@ SPLcom/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup Lcom/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenChatTailSubAssembler; SPLcom/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenChatTailSubAssembler;-><clinit>()V SPLcom/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenChatTailSubAssembler;-><init>(Lcom/vitorpamplona/quartz/nip01Core/relay/client/INostrClient;Lkotlin/jvm/functions/Function0;)V -Lcom/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsFilterAssembler; -SPLcom/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsFilterAssembler;-><clinit>()V -SPLcom/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsFilterAssembler;-><init>(Lcom/vitorpamplona/quartz/nip01Core/relay/client/INostrClient;)V -Lcom/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsFilterAssembler$group$1; -SPLcom/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsFilterAssembler$group$1;-><init>(Ljava/lang/Object;)V +Lcom/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup/RelayGroupOpenThreadsFilterAssembler; +SPLcom/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup/RelayGroupOpenThreadsFilterAssembler;-><clinit>()V +SPLcom/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup/RelayGroupOpenThreadsFilterAssembler;-><init>(Lcom/vitorpamplona/quartz/nip01Core/relay/client/INostrClient;)V +Lcom/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup/RelayGroupOpenThreadsFilterAssembler$group$1; +SPLcom/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup/RelayGroupOpenThreadsFilterAssembler$group$1;-><init>(Ljava/lang/Object;)V Lcom/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsHistoryFilterAssembler; SPLcom/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsHistoryFilterAssembler;-><clinit>()V SPLcom/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsHistoryFilterAssembler;-><init>(Lcom/vitorpamplona/quartz/nip01Core/relay/client/INostrClient;)V @@ -25535,13 +25535,13 @@ SPLcom/vitorpamplona/amethyst/commons/account/ui/login/LoginButtonKt;->LoginButt Lcom/vitorpamplona/amethyst/commons/account/ui/login/LoginButtonKt$$ExternalSyntheticLambda0; SPLcom/vitorpamplona/amethyst/commons/account/ui/login/LoginButtonKt$$ExternalSyntheticLambda0;-><init>(Z)V SPLcom/vitorpamplona/amethyst/commons/account/ui/login/LoginButtonKt$$ExternalSyntheticLambda0;->invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; -Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager; -SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager;-><clinit>()V -SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager;-><init>()V -SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager;->clearErrors()V -SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager;->getError()Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager$IErrorMsg; -SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager;->setError(Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager$IErrorMsg;)V -Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager$IErrorMsg; +Lcom/vitorpamplona/amethyst/commons/account/ui/login/LoginErrorManager; +SPLcom/vitorpamplona/amethyst/commons/account/ui/login/LoginErrorManager;-><clinit>()V +SPLcom/vitorpamplona/amethyst/commons/account/ui/login/LoginErrorManager;-><init>()V +SPLcom/vitorpamplona/amethyst/commons/account/ui/login/LoginErrorManager;->clearErrors()V +SPLcom/vitorpamplona/amethyst/commons/account/ui/login/LoginErrorManager;->getError()Lcom/vitorpamplona/amethyst/commons/account/ui/login/LoginErrorManager$IErrorMsg; +SPLcom/vitorpamplona/amethyst/commons/account/ui/login/LoginErrorManager;->setError(Lcom/vitorpamplona/amethyst/commons/account/ui/login/LoginErrorManager$IErrorMsg;)V +Lcom/vitorpamplona/amethyst/commons/account/ui/login/LoginErrorManager$IErrorMsg; Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginScreenKt; SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginScreenKt;->LoginPage$lambda$4(Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel;Lkotlin/jvm/functions/Function0;ILandroidx/compose/runtime/Composer;I)Lkotlin/Unit; SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginScreenKt;->LoginPage(Lcom/vitorpamplona/amethyst/ui/screen/AccountSessionManager;ZLjava/lang/String;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)V @@ -25575,7 +25575,7 @@ SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel;-><clinit SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel;-><init>()V SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel;->clear()V SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel;->getAcceptedTerms()Z -SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel;->getErrorManager()Lcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager; +SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel;->getErrorManager()Lcom/vitorpamplona/amethyst/commons/account/ui/login/LoginErrorManager; SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel;->getKey()Landroidx/compose/ui/text/input/TextFieldValue; SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel;->getNeedsPassword()Z SPLcom/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel;->getOfferTemporaryLogin()Z diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index fe03751d12..e2a51f8237 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -47,6 +47,7 @@ import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect import com.vitorpamplona.amethyst.commons.model.buzz.BuzzWorkspaces import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filter +import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannelListState import com.vitorpamplona.amethyst.commons.model.concord.ConcordSessionManager @@ -175,7 +176,6 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.FeedTopNavFilterState import com.vitorpamplona.amethyst.model.trustedAssertions.TrustProviderListState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.FileHeader -import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.screen.loggedIn.EventProcessor import com.vitorpamplona.marmotquic.QuicAgentTextStreamTransport import com.vitorpamplona.quartz.buzz.threading.buzzThread diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountConcordActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountConcordActions.kt index 31c41e236d..198509f959 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountConcordActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountConcordActions.kt @@ -29,8 +29,8 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.filter import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel import com.vitorpamplona.amethyst.commons.model.concord.ConcordCommunitySession +import com.vitorpamplona.amethyst.commons.model.concordChannelLastReadRoute import com.vitorpamplona.amethyst.commons.viewmodels.ReplyMode -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.concordChannelLastReadRoute import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityList.withControlRoot import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEntry import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt index 3acb2ed75a..3e6978ed94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt @@ -29,7 +29,7 @@ import com.vitorpamplona.amethyst.commons.model.navigation.navBarItemsFromNames import com.vitorpamplona.amethyst.commons.model.navigation.toNames import com.vitorpamplona.amethyst.commons.service.pow.PoWCategory import com.vitorpamplona.amethyst.commons.service.pow.PoWPolicy -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists +import com.vitorpamplona.amethyst.commons.util.equalImmutableLists import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationReplyReceiver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationReplyReceiver.kt index da7c86e2bc..4b69bfd809 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationReplyReceiver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationReplyReceiver.kt @@ -30,10 +30,10 @@ import androidx.core.content.ContextCompat import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.cancelAndPrune import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.cancelChildlessGroupSummaries -import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RelayPurposeSummary.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RelayPurposeSummary.kt index 1f7ac582a4..69d9e65b95 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RelayPurposeSummary.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RelayPurposeSummary.kt @@ -23,12 +23,12 @@ package com.vitorpamplona.amethyst.service.notifications import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.purposes +import com.vitorpamplona.amethyst.commons.relays.ui.SubPurposeLabels import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.relay_purpose_browsing import com.vitorpamplona.amethyst.commons.resources.relay_purpose_line import com.vitorpamplona.amethyst.commons.ui.loadPluralStringRes import com.vitorpamplona.amethyst.commons.ui.loadStringRes -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.SubPurposeLabels import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/NwcPaymentNotifier.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/NwcPaymentNotifier.kt index d0c217ecf8..16c5fc2538 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/NwcPaymentNotifier.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/NwcPaymentNotifier.kt @@ -24,12 +24,12 @@ import android.content.Context import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.app_notification_payments_channel_message import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.notifications.NotificationCategory import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postStandard import com.vitorpamplona.amethyst.service.notifications.notificationManager -import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransaction import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ZapNotification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ZapNotification.kt index 6f13908502..67c9c9e6cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ZapNotification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/renderers/ZapNotification.kt @@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.commons.resources.app_notification_zaps_channe import com.vitorpamplona.amethyst.commons.resources.app_notification_zaps_channel_message_for import com.vitorpamplona.amethyst.commons.resources.app_notification_zaps_channel_message_from import com.vitorpamplona.amethyst.commons.ui.loadStringRes +import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.notifications.NotificationCategory import com.vitorpamplona.amethyst.service.notifications.NotificationContent @@ -38,7 +39,6 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postStandard import com.vitorpamplona.amethyst.service.notifications.notificationManager -import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt index 12900a0d28..7291d8e943 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt @@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.badges.BadgesFilterAssembl import com.vitorpamplona.amethyst.commons.relayClient.badges.profile.ProfileBadgesFilterAssembler import com.vitorpamplona.amethyst.commons.relayClient.calendars.CalendarsFilterAssembler import com.vitorpamplona.amethyst.commons.relayClient.channel.ChannelFilterAssembler +import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.RelayGroupOpenThreadsFilterAssembler import com.vitorpamplona.amethyst.commons.relayClient.chess.ChessFilterAssembler import com.vitorpamplona.amethyst.commons.relayClient.communities.CommunityFilterAssembler import com.vitorpamplona.amethyst.commons.relayClient.communities.list.CommunitiesListFilterAssembler @@ -85,7 +86,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayG import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupJoinedStateFilterAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupOpenChatHistoryFilterAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupOpenChatTailFilterAssembler -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupOpenThreadsFilterAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupOpenThreadsHistoryFilterAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupsDiscoveryFilterAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupsOnRelayFilterAssembler diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index f7f574dbd6..b9d4cbe885 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -78,12 +78,14 @@ import com.vitorpamplona.amethyst.commons.resources.lightning_create_and_add_inv import com.vitorpamplona.amethyst.commons.resources.lightning_invoice import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.ui.components.OutlinedThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.creators.invoice.AddLnInvoiceButton import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index 3c16bf0fb7..cb9aedbd85 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -38,6 +38,8 @@ import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_de import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker +import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor @@ -45,9 +47,7 @@ import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt index d217a6f5e9..6c58b3d639 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt @@ -36,11 +36,11 @@ import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_de import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index 1522eb0928..905d049668 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -71,6 +71,7 @@ import com.vitorpamplona.amethyst.commons.resources.strip_metadata_description import com.vitorpamplona.amethyst.commons.resources.strip_metadata_label import com.vitorpamplona.amethyst.commons.resources.video_codec_h265_description import com.vitorpamplona.amethyst.commons.resources.video_codec_h265_label +import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagOutputTransformation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagOutputTransformation.kt index f6c87b44b9..8306f7eba2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagOutputTransformation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/UrlUserTagOutputTransformation.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.style.TextDecoration import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.ui.text.MENTION_REGEX import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull import kotlin.coroutines.cancellation.CancellationException diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersLIstView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersLIstView.kt index f9c6ce849e..e8c09e4a3d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersLIstView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersLIstView.kt @@ -61,6 +61,7 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.mediaServers.ServerHealth import com.vitorpamplona.amethyst.commons.mediaServers.ui.MediaServerEditField import com.vitorpamplona.amethyst.commons.model.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomServersViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomServersViewModel.kt index d5be72fc12..757b5c7023 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomServersViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomServersViewModel.kt @@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.ui.actions.mediaServers import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.mediaServers.MediaServerHealthProbe +import com.vitorpamplona.amethyst.commons.mediaServers.ServerHealth import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType import com.vitorpamplona.amethyst.commons.service.http.IRoleBasedHttpClientBuilder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordVoiceButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordVoiceButton.kt index b05ef98a56..fe3f79176a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordVoiceButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordVoiceButton.kt @@ -37,6 +37,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.audio.ExpandingCirclesAnimation +import com.vitorpamplona.amethyst.commons.audio.FloatingRecordingIndicator import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.resources.Res diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt index 9883511d35..3f3a70fb4c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.actions.uploads import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import com.vitorpamplona.amethyst.commons.audio.VoicePreset import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt index 5eb5947e91..28b2cfe15a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt @@ -32,6 +32,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.audio.VoicePreset import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.voice_anonymize_description import com.vitorpamplona.amethyst.commons.resources.voice_anonymize_title diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt index 0a53b6412e..0ea2b1c747 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt @@ -25,6 +25,8 @@ import android.media.MediaCodecInfo import android.media.MediaExtractor import android.media.MediaFormat import android.media.MediaMuxer +import com.vitorpamplona.amethyst.commons.audio.PitchShifter +import com.vitorpamplona.amethyst.commons.audio.VoicePreset import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt index 6db110e268..f43c6b5a19 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt @@ -59,6 +59,7 @@ import com.vitorpamplona.amethyst.commons.resources.recording_indicator_descript import com.vitorpamplona.amethyst.commons.resources.remove import com.vitorpamplona.amethyst.commons.ui.components.AudioWaveformReadOnly import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.util.formatSecondsToTime import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.delay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt index c0b98e8f9b..3e0ca3fbd4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.audio.VoicePreset import com.vitorpamplona.amethyst.commons.ui.stringRes @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt index c852bc1208..5087df059e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlPdf import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled +import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewCard import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.theme.HalfVertPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index bfa2899272..981807d040 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -122,6 +122,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.CashuCardBorders import com.vitorpamplona.amethyst.commons.ui.theme.HalfVertPadding import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.inlinePlaceholder +import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserNickname @@ -129,7 +130,6 @@ import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.openBlossomUriAs import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.creators.invoice.ClinkOfferPreview import com.vitorpamplona.amethyst.ui.note.creators.invoice.MayBeInvoicePreview -import com.vitorpamplona.amethyst.ui.note.toShortDisplay 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 820a1ae3b2..9c064731ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -107,6 +107,7 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.richtext.isAnimatedMediaUrl import com.vitorpamplona.amethyst.commons.service.image.placeholderModel import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled +import com.vitorpamplona.amethyst.commons.ui.components.InformationDialog import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow @@ -127,7 +128,6 @@ import com.vitorpamplona.amethyst.model.MediaAspectRatioCache import com.vitorpamplona.amethyst.service.images.BlossomFetcher import com.vitorpamplona.amethyst.service.playback.composable.VideoView import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.openBlossomUriAsIntent -import com.vitorpamplona.amethyst.ui.actions.InformationDialog import com.vitorpamplona.amethyst.ui.components.pdf.PdfPreviewCard import com.vitorpamplona.amethyst.ui.components.pdf.PdfViewerDialog import com.vitorpamplona.amethyst.ui.note.BlankNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/DisplayErrorMessages.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/DisplayErrorMessages.kt index 399c6687e1..c17b98d42c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/DisplayErrorMessages.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/DisplayErrorMessages.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.components.toasts import androidx.compose.runtime.Composable import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.ui.components.InformationDialog import com.vitorpamplona.amethyst.commons.ui.components.toasts.ActionableStringToastMsg import com.vitorpamplona.amethyst.commons.ui.components.toasts.ResourceToastMsg import com.vitorpamplona.amethyst.commons.ui.components.toasts.StringToastMsg @@ -31,7 +32,6 @@ import com.vitorpamplona.amethyst.commons.ui.components.toasts.ToastManager import com.vitorpamplona.amethyst.commons.ui.components.toasts.multiline.MultiErrorToastMsg import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.ui.actions.InformationDialog import com.vitorpamplona.amethyst.ui.components.toasts.multiline.MultiUserErrorMessageDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index cb32a317ff..79c3116611 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -51,6 +51,7 @@ import com.vitorpamplona.amethyst.commons.model.navigation.MediaFeedRoute import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.navigation.favoriteIds import com.vitorpamplona.amethyst.commons.model.navigation.isSameRoute +import com.vitorpamplona.amethyst.commons.model.navigation.limitToRouteTextArg import com.vitorpamplona.amethyst.commons.nipACWebRtcCalls.CallState import com.vitorpamplona.amethyst.commons.relayClient.event.LocalEventFinder import com.vitorpamplona.amethyst.commons.relayClient.user.LocalUserFinder @@ -59,6 +60,8 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.invalid_nip19_uri import com.vitorpamplona.amethyst.commons.resources.invalid_nip19_uri_description import com.vitorpamplona.amethyst.commons.ui.layouts.LocalScreenLayout +import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.LocalTabReselectCoordinator +import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.TabReselectCoordinator import com.vitorpamplona.amethyst.service.crashreports.DisplayCrashMessages import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.RelayAuthPromptHost import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.compose.DisplayNotifyMessages @@ -76,14 +79,11 @@ import com.vitorpamplona.amethyst.ui.call.CallActivity import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.components.toasts.DisplayErrorMessages import com.vitorpamplona.amethyst.ui.layouts.rememberScreenLayoutSpec -import com.vitorpamplona.amethyst.ui.navigation.bottombars.LocalTabReselectCoordinator -import com.vitorpamplona.amethyst.ui.navigation.bottombars.TabReselectCoordinator import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.navigation.navs.rememberNav import com.vitorpamplona.amethyst.ui.navigation.routes.consumesSharesInPlace import com.vitorpamplona.amethyst.ui.navigation.routes.getRouteWithArguments import com.vitorpamplona.amethyst.ui.navigation.routes.isBaseRoute -import com.vitorpamplona.amethyst.ui.navigation.routes.limitToRouteTextArg import com.vitorpamplona.amethyst.ui.note.PayViaIntentScreen import com.vitorpamplona.amethyst.ui.note.UpdateReactionTypeScreen import com.vitorpamplona.amethyst.ui.note.nip22Comments.ReplyCommentPostScreen diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt index c91ee5a633..ec0ca2e514 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt @@ -50,7 +50,10 @@ import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppIcon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.model.navigation.BottomBarEntry import com.vitorpamplona.amethyst.commons.model.navigation.Route +import com.vitorpamplona.amethyst.commons.ui.insets.KeyboardState +import com.vitorpamplona.amethyst.commons.ui.insets.keyboardAsState import com.vitorpamplona.amethyst.commons.ui.layouts.LocalScreenLayout +import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.LocalTabReselectCoordinator import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppNavigationRail.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppNavigationRail.kt index 30dddb074a..a4397757b0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppNavigationRail.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppNavigationRail.kt @@ -39,6 +39,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.navigation.BottomBarEntry import com.vitorpamplona.amethyst.commons.model.navigation.Route +import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.LocalTabReselectCoordinator import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.navigation.routes.getRouteWithArguments import com.vitorpamplona.amethyst.ui.navigation.topbars.LoggedInUserPictureDrawer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt index 1df6a6cfc4..f3fe6c163e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt @@ -80,8 +80,8 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.AccountPictureModifier import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp +import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo -import com.vitorpamplona.amethyst.ui.note.toShortDisplay import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedOff.AddAccountDialog diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/RememberNavs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/RememberNavs.kt index a5fb6dbef6..a9133a7c43 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/RememberNavs.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/RememberNavs.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.navigation.compose.rememberNavController import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.ObservableNav @Composable fun rememberNav(): Nav { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt index 198ff3adac..9cfc9b9ab6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom import com.vitorpamplona.amethyst.commons.model.navigation.Route +import com.vitorpamplona.amethyst.commons.model.navigation.limitToRouteTextArg import com.vitorpamplona.amethyst.commons.model.navigation.minichatRouteFor import com.vitorpamplona.amethyst.commons.model.navigation.routeFor import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Bolt12ZapGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Bolt12ZapGallery.kt index a5d895987b..ae70fc9d85 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Bolt12ZapGallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Bolt12ZapGallery.kt @@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size25dp import com.vitorpamplona.amethyst.commons.ui.theme.Size35Modifier import com.vitorpamplona.amethyst.commons.ui.theme.StdStartPadding import com.vitorpamplona.amethyst.commons.ui.theme.WidthAuthorPictureModifier +import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index a227d40fcb..651a79a79b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -105,6 +105,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.WidthAuthorPictureModifierWit import com.vitorpamplona.amethyst.commons.ui.theme.bitcoinColor import com.vitorpamplona.amethyst.commons.ui.theme.overPictureBackground import com.vitorpamplona.amethyst.commons.ui.theme.profile35dpModifier +import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAssertionsScore import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NutzapGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NutzapGallery.kt index d0198e2beb..f97e5b0712 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NutzapGallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NutzapGallery.kt @@ -48,6 +48,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp import com.vitorpamplona.amethyst.commons.ui.theme.Size35Modifier import com.vitorpamplona.amethyst.commons.ui.theme.StdStartPadding import com.vitorpamplona.amethyst.commons.ui.theme.WidthAuthorPictureModifier +import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NutzapUserSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NutzapUserSetCompose.kt index 1417cc6c83..1665f2cc2f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NutzapUserSetCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NutzapUserSetCompose.kt @@ -47,6 +47,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.Size25dp import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp +import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NutzapUserSetCard diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/OnchainZapGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/OnchainZapGallery.kt index 2c0760b419..e7db7e7b22 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/OnchainZapGallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/OnchainZapGallery.kt @@ -49,6 +49,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size25dp import com.vitorpamplona.amethyst.commons.ui.theme.Size35Modifier import com.vitorpamplona.amethyst.commons.ui.theme.StdStartPadding import com.vitorpamplona.amethyst.commons.ui.theme.WidthAuthorPictureModifier +import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt deleted file mode 100644 index f31b1448f7..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.note - -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.toHexKey - -fun ByteArray.toHexShortDisplay(): String = toHexKey().toShortDisplay() - -fun String.toShortDisplay(): String { - if (length <= 16) return this - return replaceRange(8, length - 8, "…") -} - -fun HexKey.toDisplayHexKey(): String = this.toShortDisplay() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index a74451ba8f..bc90957b86 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -114,6 +114,7 @@ import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.vitorpamplona.amethyst.commons.audio.FloatingRecordingIndicator import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder import com.vitorpamplona.amethyst.commons.hashtags.Cashu import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons @@ -206,6 +207,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.reactionBox import com.vitorpamplona.amethyst.commons.ui.theme.ripple24dp import com.vitorpamplona.amethyst.commons.ui.theme.selectedReactionBoxModifier +import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.model.MIN_ONCHAIN_ZAP_SATS import com.vitorpamplona.amethyst.model.ReactionRowAction import com.vitorpamplona.amethyst.model.ReactionRowItem @@ -226,7 +228,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo -import com.vitorpamplona.amethyst.ui.actions.uploads.FloatingRecordingIndicator import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox import com.vitorpamplona.amethyst.ui.navigation.routes.routeReplyTo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt index c964eb47d6..3e683cb40e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt @@ -49,8 +49,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ButtonPadding import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.StdPadding import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText +import com.vitorpamplona.amethyst.commons.viewmodels.MyRelayInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays.MyRelayInfo import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index af65ce3dbb..82ca56da87 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -118,6 +118,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategorySpacingModifi import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText +import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.getFragmentActivity import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index ac414ad2b2..06b87aaaa0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -112,6 +112,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.ZeroPadding import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText +import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatter.kt deleted file mode 100644 index 0e9f839b46..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatter.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.note - -import java.math.BigDecimal -import java.math.RoundingMode -import java.text.DecimalFormat - -val TenGiga = BigDecimal(10000000000) -val OneGiga = BigDecimal(1000000000) -val TenMega = BigDecimal(10000000) -val OneMega = BigDecimal(1000000) -val TenKilo = BigDecimal(10000) -val OneKilo = BigDecimal(1000) - -private val dfGBig = - object : ThreadLocal<DecimalFormat>() { - override fun initialValue() = DecimalFormat("#.#G") - } - -private val dfGSmall = - object : ThreadLocal<DecimalFormat>() { - override fun initialValue() = DecimalFormat("#.0G") - } - -private val dfMBig = - object : ThreadLocal<DecimalFormat>() { - override fun initialValue() = DecimalFormat("#.#M") - } - -private val dfMSmall = - object : ThreadLocal<DecimalFormat>() { - override fun initialValue() = DecimalFormat("#.0M") - } - -private val dfK = - object : ThreadLocal<DecimalFormat>() { - override fun initialValue() = DecimalFormat("#.#k") - } - -private val dfN = - object : ThreadLocal<DecimalFormat>() { - override fun initialValue() = DecimalFormat("#") - } - -fun showAmount(amount: BigDecimal?): String { - if (amount == null) return "" - if (amount.abs() < BigDecimal(0.01)) return "" - - return when { - amount >= TenGiga -> dfGBig.get()!!.format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP)) - amount >= OneGiga -> dfGSmall.get()!!.format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP)) - amount >= TenMega -> dfMBig.get()!!.format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP)) - amount >= OneMega -> dfMSmall.get()!!.format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP)) - amount >= TenKilo -> dfK.get()!!.format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP)) - else -> dfN.get()!!.format(amount) - } -} - -fun showAmountWithZero(amount: BigDecimal?): String { - if (amount == null) return "0" - if (amount.abs() < BigDecimal(0.01)) return "0" - - return showAmount(amount) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNote.kt index 60899cf0e4..e476f5e5df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNote.kt @@ -105,6 +105,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.grayText import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.ripple24dp import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder +import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt index d1775e6f0f..2e6e2421d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt @@ -55,6 +55,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.composer.IExpiration import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.confirm import com.vitorpamplona.amethyst.commons.resources.expiration_date_explainer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/messagefield/MessageField.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/messagefield/MessageField.kt index 763cdd05b7..bb58f52b24 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/messagefield/MessageField.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/messagefield/MessageField.kt @@ -42,8 +42,8 @@ import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import kotlinx.coroutines.delay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/DisplayPreviews.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/DisplayPreviews.kt index 502274a919..119970dbcd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/DisplayPreviews.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/DisplayPreviews.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.model.composer.PreviewState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.theme.FillWidthQuoteBorderModifier import com.vitorpamplona.amethyst.commons.ui.theme.HalfHorzPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt index 4e7aa7b8f3..2ac9b1ee4e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt @@ -43,6 +43,7 @@ import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled +import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewCard import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.state.produceCachedState @@ -50,7 +51,6 @@ import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.service.playback.composable.VideoView import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.components.DisplayUrlWithLoadingSymbol -import com.vitorpamplona.amethyst.ui.components.UrlPreviewCard import com.vitorpamplona.amethyst.ui.components.WaitAndDisplay import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/ForwardZapTo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/ForwardZapTo.kt index ad2b1c3ea5..fc59070ecb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/ForwardZapTo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/ForwardZapTo.kt @@ -49,12 +49,12 @@ import com.vitorpamplona.amethyst.commons.ui.components.OutlinedThinPaddingTextF import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.IZapField import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ZapSplitIcon import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt index 91ff84a092..50e02948d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt @@ -64,9 +64,9 @@ import com.vitorpamplona.amethyst.commons.ui.note.buttons.PostButton import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText +import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReplies -import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index f6550f6e3a..71616b6476 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -36,8 +36,13 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.composer.DraftTagState +import com.vitorpamplona.amethyst.commons.model.composer.IExpiration import com.vitorpamplona.amethyst.commons.model.composer.IZapRaiser +import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger +import com.vitorpamplona.amethyst.commons.model.composer.PreviewState import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder +import com.vitorpamplona.amethyst.commons.model.composer.toZapSplitSetup import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState @@ -46,6 +51,8 @@ import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_de import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay +import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker +import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.IAudience @@ -60,18 +67,11 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger -import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing -import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState -import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber -import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewState import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState -import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.toZapSplitSetup import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayExternalId.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayExternalId.kt index 8bc7198144..5f1e9b9626 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayExternalId.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayExternalId.kt @@ -45,12 +45,12 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.external_id_scope import com.vitorpamplona.amethyst.commons.resources.external_url_scope +import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewCard import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier -import com.vitorpamplona.amethyst.ui.components.UrlPreviewCard import com.vitorpamplona.amethyst.ui.components.rememberUrlPreviewState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index 208535d4b4..572558dc41 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -68,6 +68,7 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.composer.AudienceSelection import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList +import com.vitorpamplona.amethyst.commons.nip72ModCommunities.ui.CommunityRulesViolationBanner import com.vitorpamplona.amethyst.commons.relayClient.communities.CommunityFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.geo_post_change_place @@ -77,6 +78,7 @@ import com.vitorpamplona.amethyst.commons.resources.lightning_invoice import com.vitorpamplona.amethyst.commons.resources.post_anonymously import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind import com.vitorpamplona.amethyst.commons.resources.zapraiser +import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.ContentSensitivityExplainer @@ -98,7 +100,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier -import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chat.kt index 3f81c7b6ce..55e2bcc6a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chat.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.nip92IMeta.appendMissingImetaUrls import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt index 4bf3991117..6c4e230095 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt @@ -45,10 +45,10 @@ import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.commons.ui.theme.SmallBorder import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder +import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader -import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt index 57712a02a7..95c20d0123 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt @@ -93,6 +93,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.innerPostModifier +import com.vitorpamplona.amethyst.commons.util.equalImmutableLists import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.components.RichTextViewer @@ -109,7 +110,6 @@ import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.note.externalLinkForNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ModeratorTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Goal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Goal.kt index 0c314f5017..7d6c699e66 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Goal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Goal.kt @@ -53,12 +53,12 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Font14SP import com.vitorpamplona.amethyst.commons.ui.theme.fundraiserProgressColor import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier +import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground -import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip75ZapGoals.ZapGoalEvent import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt index cb90389db7..599b5b6438 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt @@ -49,6 +49,8 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.routeFor +import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.LiveFlag +import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.ScheduledFlag import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.live_stream_has_ended import com.vitorpamplona.amethyst.commons.resources.live_stream_is_offline @@ -61,6 +63,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText +import com.vitorpamplona.amethyst.commons.util.equalImmutableLists import com.vitorpamplona.amethyst.service.playback.composable.VideoView import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.components.MyAsyncImage @@ -71,12 +74,9 @@ import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveFlag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ScheduledFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfVideoIsOnline import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CrossfadeCheckIfVideoIsOnline import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt index bae3eb41e3..163b3bbc5d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt @@ -49,6 +49,8 @@ import androidx.core.net.toUri import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.navigation.routeFor +import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.LiveFlag +import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.ScheduledFlag import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.meeting_room_in_space import com.vitorpamplona.amethyst.commons.resources.meeting_room_in_space_unknown @@ -72,15 +74,13 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SmallBorder import com.vitorpamplona.amethyst.commons.ui.theme.SpacedBy5dp import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText +import com.vitorpamplona.amethyst.commons.util.equalImmutableLists import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveFlag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ScheduledFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CrossfadeCheckIfVideoIsOnline -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicPlaylist.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicPlaylist.kt index d3518bc2f1..cb9ddb2e41 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicPlaylist.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicPlaylist.kt @@ -62,6 +62,7 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists +import com.vitorpamplona.amethyst.commons.music.formatTrackDuration import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.music_playlist_collaborative import com.vitorpamplona.amethyst.commons.resources.music_playlist_edit_action diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicTrack.kt index ec896a8963..026f8c118c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicTrack.kt @@ -58,6 +58,7 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists +import com.vitorpamplona.amethyst.commons.music.formatTrackDuration import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.preview_card_image_for import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nutzap.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nutzap.kt index a6a0843de2..071875487c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nutzap.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nutzap.kt @@ -38,10 +38,10 @@ import com.vitorpamplona.amethyst.commons.ui.note.ActivityCardFrame import com.vitorpamplona.amethyst.commons.ui.note.ActivityHeaderRow import com.vitorpamplona.amethyst.commons.ui.theme.Size25dp import com.vitorpamplona.amethyst.commons.ui.theme.bitcoinColor +import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.ui.note.CrossfadeToDisplayComment import com.vitorpamplona.amethyst.ui.note.DisplayBlankAuthor import com.vitorpamplona.amethyst.ui.note.UserPicture -import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.claimedSatsTotal diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt index 4ed610bed1..d4ffca2d73 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.feeds.UserFeedState import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled import com.vitorpamplona.amethyst.commons.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt index d0ee9ac934..953e7058cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt @@ -27,11 +27,12 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.feeds.FeedFilter import com.vitorpamplona.amethyst.commons.feeds.InvalidatableContent +import com.vitorpamplona.amethyst.commons.feeds.UserFeedState import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.service.BundledUpdate +import com.vitorpamplona.amethyst.commons.util.equalImmutableLists import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.quartz.utils.Log import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index faa2f7b8af..8f49870418 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -50,6 +50,7 @@ import com.vitorpamplona.amethyst.commons.model.UiSettingsFlow import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.backups.ReplaceableBackupConflict import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.navigation.BottomBarEntry @@ -110,6 +111,9 @@ import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.notifications.CardFeedState import com.vitorpamplona.amethyst.commons.ui.state.GenericBaseCache import com.vitorpamplona.amethyst.commons.ui.state.GenericBaseCacheAsync +import com.vitorpamplona.amethyst.commons.util.DebouncedPublisher +import com.vitorpamplona.amethyst.commons.util.showAmount +import com.vitorpamplona.amethyst.commons.util.showAmountInteger import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountSettings @@ -127,12 +131,9 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.dismis import com.vitorpamplona.amethyst.service.pow.powKindLabelRes import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk -import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus import com.vitorpamplona.amethyst.ui.note.payViaIntent -import com.vitorpamplona.amethyst.ui.note.showAmount -import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotGroupIconChange import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotGroupIconUpload import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotGroupIconUploader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/dal/ArticlesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/dal/ArticlesFeedFilter.kt index 7bea4bb080..56bdbdcce6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/dal/ArticlesFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/articles/dal/ArticlesFeedFilter.kt @@ -21,12 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.articles.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/dal/BadgesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/dal/BadgesFeedFilter.kt index 9f650a9a2a..3425254468 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/dal/BadgesFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/dal/BadgesFeedFilter.kt @@ -21,12 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.badges.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeDialog.kt index e3d2855357..b417ccb3d4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeDialog.kt @@ -83,6 +83,7 @@ import com.vitorpamplona.amethyst.commons.resources.media_compression_quality_un import com.vitorpamplona.amethyst.commons.resources.new_badge import com.vitorpamplona.amethyst.commons.resources.strip_metadata_description import com.vitorpamplona.amethyst.commons.resources.strip_metadata_label +import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe @@ -91,7 +92,6 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.Settin import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelect import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeModel.kt index 5a96572326..b59ed9928f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/badges/post/NewBadgeModel.kt @@ -34,11 +34,11 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events import com.vitorpamplona.amethyst.commons.resources.read_only_user +import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt index 580444c5c0..93bda944b7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardScreen.kt @@ -88,6 +88,9 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.buzz.AgentWorkItem +import com.vitorpamplona.amethyst.commons.model.buzz.AgentWorkKind +import com.vitorpamplona.amethyst.commons.model.buzz.AgentWorkState import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.note.elements.TimeAgo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardViewModel.kt index fe0d4e9ebf..49ab04252a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWorkBoardViewModel.kt @@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.model.buzz.AgentWorkBoard +import com.vitorpamplona.amethyst.commons.model.buzz.AgentWorkItem import com.vitorpamplona.amethyst.commons.model.buzz.BuzzJobAggregator import com.vitorpamplona.amethyst.commons.model.buzz.WorkflowRunAggregator import com.vitorpamplona.amethyst.model.Account diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzForumPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzForumPostScreen.kt index d3379eb592..de6e21895d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzForumPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzForumPostScreen.kt @@ -42,6 +42,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.buzz_forum_body_label import com.vitorpamplona.amethyst.commons.resources.buzz_forum_new_title @@ -49,7 +50,6 @@ import com.vitorpamplona.amethyst.commons.resources.buzz_forum_post_action import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.buzz.forum.ForumPostEvent import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/ForumReplyNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/ForumReplyNewMessageViewModel.kt index 55f0a6984d..d7845566f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/ForumReplyNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/ForumReplyNewMessageViewModel.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.buzz +import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel -import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel import com.vitorpamplona.quartz.buzz.forum.ForumCommentEvent import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsScreen.kt index e498d73b81..5b019c2ac3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsScreen.kt @@ -31,6 +31,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.model.navigation.Route +import com.vitorpamplona.amethyst.commons.nip52Calendar.ui.CalendarsViewMode import com.vitorpamplona.amethyst.commons.nip52Calendar.ui.NewCalendarButton import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsTopBar.kt index 433f7224b8..3ad0fc5182 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsTopBar.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter +import com.vitorpamplona.amethyst.commons.nip52Calendar.ui.CalendarsViewMode import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.select_list_to_filter import com.vitorpamplona.amethyst.commons.search.SearchSeed diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsViewModel.kt index 5b41ea50f8..fe401d2b98 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsViewModel.kt @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip52Calendar.MonthGridBarSegment import com.vitorpamplona.amethyst.commons.model.nip52Calendar.computeMonthGridBars import com.vitorpamplona.amethyst.commons.model.nip52Calendar.groupByDayKeyExpanded +import com.vitorpamplona.amethyst.commons.nip52Calendar.ui.CalendarsViewMode import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/dal/CalendarAppointmentsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/dal/CalendarAppointmentsFeedFilter.kt index 65ef31262e..50b42f8a00 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/dal/CalendarAppointmentsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/dal/CalendarAppointmentsFeedFilter.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -28,7 +29,6 @@ import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.nip52Calendar.upcomingFirstCalendarOrder import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/dal/CalendarCollectionsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/dal/CalendarCollectionsFeedFilter.kt index 7396b10d5a..ef43b1e29b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/dal/CalendarCollectionsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/dal/CalendarCollectionsFeedFilter.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnComposer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnComposer.kt index c892f3d020..a09a212e37 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnComposer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnComposer.kt @@ -49,6 +49,7 @@ import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestio import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.cordn_composer_hint +import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldBorder @@ -56,7 +57,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldModifier import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt index 25519ffbb2..69b60bc6ff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt @@ -42,13 +42,13 @@ import com.vitorpamplona.amethyst.commons.chats.ui.watchChatGroupPosition import com.vitorpamplona.amethyst.commons.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.composer.DraftTagState import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled import com.vitorpamplona.amethyst.commons.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding -import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/LoadingReplyNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/LoadingReplyNote.kt index 850f26e70b..bcf67f43bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/LoadingReplyNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/LoadingReplyNote.kt @@ -48,6 +48,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.model.chats.formatHistoryReachDate import com.vitorpamplona.amethyst.commons.relayClient.paging.PagingStatus import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.chats_reply_not_found diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt index c52b22a690..b3d0c85a11 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChatZap.kt @@ -48,12 +48,12 @@ import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp import com.vitorpamplona.amethyst.commons.ui.theme.Size24Modifier +import com.vitorpamplona.amethyst.commons.util.showAmountInteger import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.CrossfadeToDisplayComment import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification -import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt index ba2c0c0dfb..b28fd4501d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.commons.chats.ui.jumboEmojiCount import com.vitorpamplona.amethyst.commons.chats.ui.jumboEmojiFontSize import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.nip92IMeta.appendMissingImetaUrls import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.could_not_decrypt_the_message @@ -39,7 +40,6 @@ import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContentOrNull import com.vitorpamplona.amethyst.ui.note.types.RenderAudioFromIMeta -import com.vitorpamplona.amethyst.ui.note.types.appendMissingImetaUrls import com.vitorpamplona.amethyst.ui.note.types.getAudioMetaWithWaveform import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt index 1a480a9adc..4269133bc2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupChatView.kt @@ -50,6 +50,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.chats.ui.ThinSendButton import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.marmotGroupLastReadRoute import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.marmot_group_composer_disbanding import com.vitorpamplona.amethyst.commons.resources.marmot_group_composer_leaving @@ -61,13 +62,13 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldBorder import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldModifier import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt index 7894287c53..d94e18af51 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupListScreen.kt @@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.commons.chats.ui.ChatUnreadBadge import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.marmotGroupLastReadRoute import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt index 14f7c0528b..611bf714b2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt @@ -41,6 +41,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.chats.formatHistoryReachDate import com.vitorpamplona.amethyst.commons.model.privateChatLastReadRoute import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.chats_history_proto_nip04 @@ -61,7 +62,6 @@ import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.elements.ObserveRelayListForDMsAndDisplayIfNotFound import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.formatHistoryReachDate import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.dal.ChatroomFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.datasource.ChatroomFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.DmReportWarningCard diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index ca797a2b12..889e7ded8a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -36,10 +36,16 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.composer.DraftTagState +import com.vitorpamplona.amethyst.commons.model.composer.IExpiration import com.vitorpamplona.amethyst.commons.model.composer.IZapRaiser +import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger +import com.vitorpamplona.amethyst.commons.model.composer.PreviewState import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder +import com.vitorpamplona.amethyst.commons.model.composer.toZapSplitSetup import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState +import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.IZapField import com.vitorpamplona.amethyst.commons.ui.text.currentWord @@ -48,15 +54,9 @@ import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.location.LocationState -import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation -import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState -import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber -import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewState import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState -import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.toZapSplitSetup import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileSender import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index 5b32232905..38c55eadf4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt @@ -104,6 +104,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraise import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.Font14SP @@ -113,7 +114,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index bb752cb228..11b011c810 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -62,10 +62,12 @@ import com.vitorpamplona.amethyst.commons.resources.reply_here import com.vitorpamplona.amethyst.commons.resources.retry_without_encryption import com.vitorpamplona.amethyst.commons.resources.this_message_will_disappear_in import com.vitorpamplona.amethyst.commons.resources.upload_without_encryption_warning +import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldBorder import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldModifier @@ -77,8 +79,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SpacedBy10dp import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation -import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt index fc1c1f025b..81f5853241 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelScreen.kt @@ -59,7 +59,9 @@ import com.vitorpamplona.amethyst.commons.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.chats.formatHistoryReachDate import com.vitorpamplona.amethyst.commons.model.concord.ConcordCommunitySession +import com.vitorpamplona.amethyst.commons.model.concordChannelLastReadRoute import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.back @@ -80,6 +82,7 @@ import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldBorder import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldModifier @@ -87,7 +90,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo -import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia @@ -96,7 +98,6 @@ import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoad import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.formatHistoryReachDate import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploader import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.SuccessfulUploads import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelHistorySubAssembler diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordUnread.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordUnread.kt index 93bca99702..82b63aa77e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordUnread.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordUnread.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel +import com.vitorpamplona.amethyst.commons.model.concordChannelLastReadRoute import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.concord.cord03Channels.ConcordChannelId import com.vitorpamplona.quartz.nip22Comments.CommentEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamTopZappers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamTopZappers.kt index 9744b58ed0..7d31a80b08 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamTopZappers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveStreamTopZappers.kt @@ -60,9 +60,9 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.commons.ui.theme.Size16Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size24dp +import com.vitorpamplona.amethyst.commons.util.showAmountInteger import com.vitorpamplona.amethyst.commons.viewmodels.LiveStreamTopZappersViewModel import com.vitorpamplona.amethyst.ui.note.UserPicture -import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import java.math.BigDecimal diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt index fc50788692..99a26c721d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt @@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.elements.NormalTimeAgo import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.Size25dp +import com.vitorpamplona.amethyst.commons.util.equalImmutableLists import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor @@ -65,7 +66,6 @@ import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt index 7cf7e4a9ce..7279559775 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt @@ -39,6 +39,8 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.LiveFlag +import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.OfflineFlag import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.theme.RowColSpacing import com.vitorpamplona.amethyst.commons.ui.theme.Size34dp @@ -49,8 +51,6 @@ import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveFlag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.OfflineFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfVideoIsOnline import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelView.kt index 9a6aec581c..18f185b258 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupChannelView.kt @@ -43,7 +43,9 @@ import com.vitorpamplona.amethyst.commons.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect +import com.vitorpamplona.amethyst.commons.model.chats.formatHistoryReachDate import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel +import com.vitorpamplona.amethyst.commons.model.relayGroupChannelLastReadRoute import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.relay_group_invite_only_to_post import com.vitorpamplona.amethyst.commons.resources.relay_group_join_to_post @@ -58,7 +60,6 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.formatHistoryReachDate import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupOpenChatHistorySubAssembler diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupDiscoveryScreen.kt index b2125dac37..f2c36607d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupDiscoveryScreen.kt @@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.navigation.routeFor import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel +import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.toGroupConstraints import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.relay_group_badge_invite_only import com.vitorpamplona.amethyst.commons.resources.relay_group_badge_live @@ -99,7 +100,6 @@ import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.dal.relayGroupDiscoveryChannelFor -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.dal.toGroupConstraints import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupCardWarmupSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource.RelayGroupsDiscoveryFilterAssemblerSubscription import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupUnread.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupUnread.kt index 3937effabe..1a8ef9e314 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupUnread.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupUnread.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.chats.isMinichatReply import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel +import com.vitorpamplona.amethyst.commons.model.relayGroupChannelLastReadRoute import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/dal/RelayGroupDiscoveryFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/dal/RelayGroupDiscoveryFeedFilter.kt index afcb987b7e..7aec5941ff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/dal/RelayGroupDiscoveryFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/dal/RelayGroupDiscoveryFeedFilter.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.GroupDiscoveryConstraint import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel +import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.toGroupConstraints import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.nip11RelayInfo.isRelaySignedRelayGroup diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsSubscription.kt index 154db5d648..f94b833729 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsSubscription.kt @@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relay import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel +import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.RelayGroupOpenThreadsFilterAssembler +import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.RelayGroupOpenThreadsQueryState import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip29RelayGroups.GroupId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsDiscoverySubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsDiscoverySubAssembler.kt index 23715d84f2..d4d8353db7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsDiscoverySubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsDiscoverySubAssembler.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsByAuthors +import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsDiscovery import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.relayGroupChannelsByRelay import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.scopedTo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 993ee2c827..cd4edf2318 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -38,6 +38,9 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.composer.DraftTagState +import com.vitorpamplona.amethyst.commons.model.composer.IExpiration +import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel @@ -53,6 +56,7 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.richtext.UrlParser import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay +import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor @@ -62,12 +66,8 @@ import com.vitorpamplona.amethyst.commons.viewmodels.ReplyMode import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState -import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt index 803b0ef134..a1093b684d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt @@ -53,16 +53,16 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.buzz_editing_banner import com.vitorpamplona.amethyst.commons.resources.cancel import com.vitorpamplona.amethyst.commons.resources.reply_here +import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldBorder import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldModifier import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation -import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 5556468cd8..514b461e3f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -54,10 +54,14 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.chatMessageMarksRoomAsRead +import com.vitorpamplona.amethyst.commons.model.chats.ConcordServerRoomNote +import com.vitorpamplona.amethyst.commons.model.chats.RelayGroupServerRoomNote import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel +import com.vitorpamplona.amethyst.commons.model.concordChannelLastReadRoute import com.vitorpamplona.amethyst.commons.model.cordnGroups.CordnGroupChatroom import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel +import com.vitorpamplona.amethyst.commons.model.marmotGroupLastReadRoute import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.navigation.routeFor @@ -66,6 +70,7 @@ import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChann import com.vitorpamplona.amethyst.commons.model.privateChatLastReadRoute import com.vitorpamplona.amethyst.commons.model.privateChats.ChatPreview import com.vitorpamplona.amethyst.commons.model.privateChats.chatPreviewOf +import com.vitorpamplona.amethyst.commons.model.relayGroupChannelLastReadRoute import com.vitorpamplona.amethyst.commons.nip29RelayGroups.ui.RelayNameChip import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.add_to_messages @@ -139,19 +144,14 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.buzzTimelinePreviewSummary import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.observeUserNameByHex import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.loadMarmotRelayIcon -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.marmotGroupLastReadRoute import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.rememberMarmotGroupAvatarUrl import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.RoomNameDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.reportWarningContentDescription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.ConcordLeaveDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.concordChannelLastReadRoute import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.concordCommunityHasUnreadFlow import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.rememberConcordImageModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.LoadEphemeralChatChannel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.relayGroupChannelLastReadRoute import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.relayGroupServerHasUnreadFlow -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.ConcordServerRoomNote -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.RelayGroupServerRoomNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn.coordinatorDisplayName import com.vitorpamplona.quartz.buzz.notifications.MemberAddedNotificationEvent import com.vitorpamplona.quartz.cordn.appEncryptedMedia.CordnMediaTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/MarkChatRoomsAsRead.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/MarkChatRoomsAsRead.kt index 488860dee0..48914bf464 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/MarkChatRoomsAsRead.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/MarkChatRoomsAsRead.kt @@ -21,17 +21,17 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.chats.ConcordServerRoomNote +import com.vitorpamplona.amethyst.commons.model.chats.RelayGroupServerRoomNote import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel +import com.vitorpamplona.amethyst.commons.model.concordChannelLastReadRoute import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel +import com.vitorpamplona.amethyst.commons.model.marmotGroupLastReadRoute import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.model.privateChatLastReadRoute +import com.vitorpamplona.amethyst.commons.model.relayGroupChannelLastReadRoute import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.marmotGroupLastReadRoute -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.concordChannelLastReadRoute -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.relayGroupChannelLastReadRoute -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.ConcordServerRoomNote -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.RelayGroupServerRoomNote import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/RowUnread.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/RowUnread.kt index 64a741db99..7b2a052e2e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/RowUnread.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/RowUnread.kt @@ -21,20 +21,20 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.chats.ConcordServerRoomNote +import com.vitorpamplona.amethyst.commons.model.chats.RelayGroupServerRoomNote import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel +import com.vitorpamplona.amethyst.commons.model.concordChannelLastReadRoute import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel +import com.vitorpamplona.amethyst.commons.model.marmotGroupLastReadRoute import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.model.publicChatChannelIdOf +import com.vitorpamplona.amethyst.commons.model.relayGroupChannelLastReadRoute import com.vitorpamplona.amethyst.commons.model.unreadPrivateChatRoute import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.marmotGroupLastReadRoute -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.concordChannelLastReadRoute import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.concordCommunityHasUnreadFlow -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.relayGroupChannelLastReadRoute import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.relayGroupServerHasUnreadFlow -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.ConcordServerRoomNote -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.RelayGroupServerRoomNote import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChatEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt index 1ba516916f..f3e9c55adf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt @@ -24,6 +24,8 @@ import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.chats.ChatFeedType +import com.vitorpamplona.amethyst.commons.model.chats.ConcordServerRoomNote +import com.vitorpamplona.amethyst.commons.model.chats.RelayGroupServerRoomNote import com.vitorpamplona.amethyst.commons.model.concord.ConcordChannel import com.vitorpamplona.amethyst.commons.model.concord.ConcordViewMode import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListNip04HistorySubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListNip04HistorySubAssembler.kt index 02af582f7a..976c38b64f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListNip04HistorySubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListNip04HistorySubAssembler.kt @@ -21,6 +21,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource import com.vitorpamplona.amethyst.commons.model.chats.ChatFeedType +import com.vitorpamplona.amethyst.commons.relayClient.chatrooms.filterNip04DMsFromMe +import com.vitorpamplona.amethyst.commons.relayClient.chatrooms.filterNip04DMsToMe import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.commons.relayClient.paging.BackwardRelayPager import com.vitorpamplona.amethyst.commons.relayClient.paging.PagingStatus diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListNip04SubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListNip04SubAssembler.kt index f4349abb05..6eb9f65478 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListNip04SubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListNip04SubAssembler.kt @@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.chats.ChatFeedType import com.vitorpamplona.amethyst.commons.model.privateChats.DmHistoryTuning +import com.vitorpamplona.amethyst.commons.relayClient.chatrooms.filterNip04DMsFromMe +import com.vitorpamplona.amethyst.commons.relayClient.chatrooms.filterNip04DMsToMe import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.commons.relayClient.paging.WindowLoadTracker import com.vitorpamplona.amethyst.commons.relayClient.paging.trackingListener diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt index b4d5033d8b..c13664a974 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.chats.ChatFeedType +import com.vitorpamplona.amethyst.commons.relayClient.chatrooms.filterFollowingEphemeralChats import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.launchChatFeedToggleObserver diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt index 0cc193ea4e..d4ad165145 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt @@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.chats.ChatFeedType +import com.vitorpamplona.amethyst.commons.relayClient.chatrooms.filterFollowingPublicChatsCreationEvent +import com.vitorpamplona.amethyst.commons.relayClient.chatrooms.filterLastMessageFollowingPublicChats import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.launchChatFeedToggleObserver diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt index 502d424d8c..f17261abee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt @@ -41,6 +41,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.feeds.FeedState import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.chats.formatHistoryReachDate import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom import com.vitorpamplona.amethyst.commons.relayClient.paging.PagingStatus import com.vitorpamplona.amethyst.commons.resources.Res @@ -64,7 +65,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.formatHistoryReachDate import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChatroomHeaderCompose import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt index 5e37b9a13f..21776a66c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt @@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.commons.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.TwoPaneNav import com.vitorpamplona.amethyst.commons.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMScreen.kt index 9c0e0a9566..6be8af6f4d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMScreen.kt @@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.resources.share_to_dm_start_new import com.vitorpamplona.amethyst.commons.resources.share_to_dm_title import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.ShareToDMNav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.ShorterTopAppBar import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt index ac0c7a21c3..2c6327533a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt @@ -27,8 +27,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/dal/ChessFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/dal/ChessFeedFilter.kt index 1afe8bcf7e..8380fc7400 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/dal/ChessFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/dal/ChessFeedFilter.kt @@ -21,10 +21,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip64Chess.game.ChessGameEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedFilter.kt index cca44e07e7..9970d5352d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityFeedFilter.kt @@ -24,6 +24,8 @@ import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.nip72Communities.latestCommunityRules +import com.vitorpamplona.amethyst.commons.model.nip72Communities.violatesCommunityRules import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedFilter.kt index ea5403ca0b..044eb8f25a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityModerationFeedFilter.kt @@ -24,6 +24,8 @@ import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.nip72Communities.latestCommunityRules +import com.vitorpamplona.amethyst.commons.model.nip72Communities.violatesCommunityRules import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/dal/CommunitiesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/dal/CommunitiesFeedFilter.kt index 7469843e02..d60215b38a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/dal/CommunitiesFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/list/dal/CommunitiesFeedFilter.kt @@ -20,12 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.list.dal +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.mapNotNullIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.DiscoverCommunityFeedFilter import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityModel.kt index 576c4cd584..981d691248 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityModel.kt @@ -37,11 +37,11 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events import com.vitorpamplona.amethyst.commons.resources.read_only_user +import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt index 0f9b946033..eac78f2eb4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/newCommunity/NewCommunityScreen.kt @@ -93,6 +93,7 @@ import com.vitorpamplona.amethyst.commons.resources.new_community_rules import com.vitorpamplona.amethyst.commons.resources.new_community_rules_section import com.vitorpamplona.amethyst.commons.resources.remove import com.vitorpamplona.amethyst.commons.resources.save +import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.ui.components.Nip05OrPubkeyLine import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -101,7 +102,6 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.CreatingTopBar import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage -import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelect import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery import com.vitorpamplona.amethyst.ui.note.UserPicture diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt index 33fdf104ce..49df00f59d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt @@ -21,12 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt index 2ece98c9c1..bc4b838b8e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostScreen.kt @@ -101,6 +101,7 @@ import com.vitorpamplona.amethyst.commons.resources.url_slug import com.vitorpamplona.amethyst.commons.resources.url_slug_placeholder import com.vitorpamplona.amethyst.commons.resources.your_article_title import com.vitorpamplona.amethyst.commons.resources.zapraiser +import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.creators.contentWarning.ContentSensitivityExplainer @@ -114,13 +115,12 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraise import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation -import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt index e076080373..4b10bc1c3c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt @@ -37,8 +37,12 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.composer.DraftTagState +import com.vitorpamplona.amethyst.commons.model.composer.IExpiration import com.vitorpamplona.amethyst.commons.model.composer.IZapRaiser +import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder +import com.vitorpamplona.amethyst.commons.model.composer.toZapSplitSetup import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.EmojiMedia @@ -49,6 +53,8 @@ import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_ import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.resources.server_did_not_provide_a_url_after_uploading import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay +import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker +import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.IZapField @@ -62,19 +68,13 @@ import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader -import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger -import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing -import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState -import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState -import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.toZapSplitSetup import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt index 3bb50c255f..3c2f9df44d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt index fda82b5723..481db912e5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt @@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.commons.ui.theme.RowColSpacing import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.grayText +import com.vitorpamplona.amethyst.commons.util.equalImmutableLists import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.ui.note.Gallery @@ -68,7 +69,6 @@ import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt index 1e0fdcad4d..a1a3eb2311 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt @@ -21,12 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt index 8b7e5cb5e5..6189aa50e5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.ParticipantListBuilder import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -34,7 +35,6 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.Mute import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.commons.service.OnlineChecker import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt index 246fadcb6d..d66acadf03 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt @@ -52,22 +52,22 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.Aut import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter +import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.EndedFlag +import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.LiveFlag +import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.OfflineFlag +import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.ScheduledFlag import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder +import com.vitorpamplona.amethyst.commons.util.equalImmutableLists import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.ui.note.Gallery import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.EndedFlag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveFlag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.OfflineFlag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ScheduledFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header.LiveActivitiesChannelHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfVideoIsOnline -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip53LiveActivities.LiveStreamLike diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt index 43dfdebcbe..f99364c6a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt @@ -63,13 +63,13 @@ import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.commons.ui.theme.RowColSpacing import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.grayText +import com.vitorpamplona.amethyst.commons.util.equalImmutableLists import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.ui.note.Gallery import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt index d817affb56..baa0d09d50 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt @@ -21,12 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.mapNotNullIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt index 12d37d180c..e337baf17c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.ParticipantListBuilder import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -35,7 +36,6 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.community. import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt index 396cb8b7d3..8b71b86826 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt @@ -47,9 +47,9 @@ import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp +import com.vitorpamplona.amethyst.commons.util.showAmountInteger import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner -import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt index 79d9ebed30..c84934f8c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt @@ -21,12 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent open class DiscoverMarketplaceFeedFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index d92f87629f..8814d25bbb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -52,6 +52,7 @@ import com.vitorpamplona.amethyst.commons.resources.lightning_create_and_add_inv import com.vitorpamplona.amethyst.commons.resources.lightning_invoice import com.vitorpamplona.amethyst.commons.resources.new_product import com.vitorpamplona.amethyst.commons.resources.zapraiser +import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar @@ -71,7 +72,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage -import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index eaa34fa87d..9b588113cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -36,13 +36,20 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.composer.DraftTagState +import com.vitorpamplona.amethyst.commons.model.composer.IExpiration import com.vitorpamplona.amethyst.commons.model.composer.IZapRaiser +import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger +import com.vitorpamplona.amethyst.commons.model.composer.PreviewState import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder +import com.vitorpamplona.amethyst.commons.model.composer.toZapSplitSetup import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details +import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker +import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.IZapField @@ -54,18 +61,11 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger -import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing -import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState -import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber -import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewState import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState -import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.toZapSplitSetup import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt index 9284dfbe4f..065426107c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SellProduct.kt @@ -83,6 +83,7 @@ import com.vitorpamplona.amethyst.commons.resources.classifieds_title_placeholde import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.Font14SP import com.vitorpamplona.amethyst.commons.ui.theme.Height100Modifier @@ -90,7 +91,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.SquaredQuoteBorderModifier import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryResponseFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryResponseFilter.kt index b9c8653a49..5ba64e8d5f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryResponseFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/DvmContentDiscoveryResponseFilter.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.observables.CreatedAtComparator import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent import com.vitorpamplona.quartz.nip90Dvms.contentDiscoveryResponse.DvmContentDiscoveryResponseEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/browse/dal/BrowseEmojiSetsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/browse/dal/BrowseEmojiSetsFeedFilter.kt index 327d5e81d6..ded1c94b30 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/browse/dal/BrowseEmojiSetsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/emojipacks/browse/dal/BrowseEmojiSetsFeedFilter.kt @@ -21,12 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.browse.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt index 2a56243e86..c56e60de18 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedConversationsFeedFilter.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -29,7 +30,6 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allUserFollows.AllUs import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt index 26a3f6bd6f..5de1c944b7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.feeds.isRenderableRepost import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note @@ -29,7 +30,6 @@ import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allUserFollows.AllUserFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allUserFollows.AllUserFollowsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.agora.FundraiserEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/dal/FollowPacksFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/dal/FollowPacksFeedFilter.kt index b4f0a54144..d42d44d8ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/dal/FollowPacksFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/list/dal/FollowPacksFeedFilter.kt @@ -20,9 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.dal +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.DiscoverFollowSetsFeedFilter class FollowPacksFeedFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocacheHuntsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocacheHuntsFeedFilter.kt index a836bbd6b1..c052a0fa78 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocacheHuntsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocacheHuntsFeedFilter.kt @@ -21,12 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nipCCGeocaching.curation.GeocacheCurationListEvent /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocacheMineFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocacheMineFeedFilter.kt index e38fd625c9..6bb4e8a551 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocacheMineFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocacheMineFeedFilter.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet +import com.vitorpamplona.amethyst.commons.relayClient.geocaches.GeocacheListingKinds import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.nipCCGeocaching.listing.GeocacheListingEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocachesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocachesFeedFilter.kt index 7604b35a7a..b1e60aaaad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocachesFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocachesFeedFilter.kt @@ -21,12 +21,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter +import com.vitorpamplona.amethyst.commons.relayClient.geocaches.GeocacheListingKinds import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nipCCGeocaching.listing.GeocacheListingEvent /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocachesHereFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocachesHereFeedFilter.kt index 6ce924b5fd..0713842aca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocachesHereFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocachesHereFeedFilter.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet +import com.vitorpamplona.amethyst.commons.relayClient.geocaches.GeocacheListingKinds import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.nipCCGeocaching.listing.GeocacheListingEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/dal/GitRepositoriesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/dal/GitRepositoriesFeedFilter.kt index e9a34e4ad9..298bea2bfb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/dal/GitRepositoriesFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/dal/GitRepositoriesFeedFilter.kt @@ -21,12 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFeedFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFeedFilterSubAssembler.kt index aa7297b134..9a7425b921 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFeedFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFeedFilterSubAssembler.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUniqueIdEoseManager +import com.vitorpamplona.amethyst.commons.relayClient.hashtag.filterPostsByHashtags import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightPostViewModel.kt index 573cbbe5da..14f8152590 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/NewHighlightPostViewModel.kt @@ -30,13 +30,13 @@ import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.EmojiMedia import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/dal/HighlightsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/dal/HighlightsFeedFilter.kt index 6fa403c631..d0eeb79514 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/dal/HighlightsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/highlights/dal/HighlightsFeedFilter.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.highlights.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 142a83683e..031fc4226f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -110,6 +110,7 @@ import com.vitorpamplona.amethyst.commons.resources.schedule_post_always_on_prom import com.vitorpamplona.amethyst.commons.resources.schedule_post_always_on_prompt_title import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind import com.vitorpamplona.amethyst.commons.resources.zapraiser +import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.note.creators.aihelp.AiWritingHelpPanel @@ -137,7 +138,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPa import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier -import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS import com.vitorpamplona.amethyst.ui.actions.uploads.RecordVoiceButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 625d16fd37..e4733e4694 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -34,13 +34,19 @@ import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.audio.VoicePreset import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.BooleanType import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.composer.DraftTagState +import com.vitorpamplona.amethyst.commons.model.composer.IExpiration import com.vitorpamplona.amethyst.commons.model.composer.IZapRaiser +import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger +import com.vitorpamplona.amethyst.commons.model.composer.PreviewState import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder +import com.vitorpamplona.amethyst.commons.model.composer.toZapSplitSetup import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.EmojiMedia import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState @@ -59,6 +65,8 @@ import com.vitorpamplona.amethyst.commons.service.ai.WritingAssistantStatus import com.vitorpamplona.amethyst.commons.service.ai.WritingResult import com.vitorpamplona.amethyst.commons.service.ai.WritingTone import com.vitorpamplona.amethyst.commons.service.pow.PoWReplay +import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker +import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.IAudience @@ -76,22 +84,14 @@ import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadingState -import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger -import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationController -import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePreset -import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState -import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber -import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewState import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState -import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.toZapSplitSetup import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index c1b223f5fb..2ae2f26bf4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.audio.VoicePreset import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName @@ -44,7 +45,6 @@ import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationController -import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePreset import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.tags.people.toPTag import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt index 146ceae389..3479a85771 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt @@ -21,13 +21,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.HomeFeedType import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt index 886dd9671a..2448f89a4e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveComplexFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -34,7 +35,6 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.community. import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChatEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt index 1cf99188dd..288ea25da9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.feeds.isRenderableRepost import com.vitorpamplona.amethyst.commons.model.HomeFeedType import com.vitorpamplona.amethyst.commons.model.Note @@ -29,7 +30,6 @@ import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.agora.FundraiserEvent import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/livestreams/dal/LiveStreamsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/livestreams/dal/LiveStreamsFeedFilter.kt index 5fe9419106..4c70fbfb1a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/livestreams/dal/LiveStreamsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/livestreams/dal/LiveStreamsFeedFilter.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.ParticipantListBuilder import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -34,7 +35,6 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.Mute import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.commons.service.OnlineChecker import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/dal/LongsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/dal/LongsFeedFilter.kt index 138d6b8e34..a8b24ae923 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/dal/LongsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/longs/dal/LongsFeedFilter.kt @@ -21,6 +21,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams +import com.vitorpamplona.amethyst.commons.feeds.SupportedContent import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -29,9 +31,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.commons.relayClient.video.SUPPORTED_VIDEO_FEED_MIME_TYPES_SET import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder -import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal.SupportedContent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip71Video.AddressableNormalVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoMeta diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt index 8ef8fa212d..c185009a62 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistScreen.kt @@ -92,6 +92,7 @@ import com.vitorpamplona.amethyst.commons.resources.music_playlist_track_count import com.vitorpamplona.amethyst.commons.resources.music_playlist_unknown_track import com.vitorpamplona.amethyst.commons.resources.music_playlist_uploading_banner import com.vitorpamplona.amethyst.commons.resources.new_music_playlist +import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -100,7 +101,6 @@ import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent -import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistViewModel.kt index 5c7c115a8d..9b1d06e1fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicPlaylistViewModel.kt @@ -25,12 +25,12 @@ import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName +import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt index 354b50506b..53f6016435 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackScreen.kt @@ -83,13 +83,13 @@ import com.vitorpamplona.amethyst.commons.resources.music_track_title_label import com.vitorpamplona.amethyst.commons.resources.music_track_title_placeholder import com.vitorpamplona.amethyst.commons.resources.music_track_uploading_banner import com.vitorpamplona.amethyst.commons.resources.new_music_track +import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SendingTopBar import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn -import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt index a0878ce79a..936a63a29a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/NewMusicTrackViewModel.kt @@ -28,12 +28,12 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName +import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicPlaylistsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicPlaylistsFeedFilter.kt index 42746dd5c3..818d11805d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicPlaylistsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicPlaylistsFeedFilter.kt @@ -21,12 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.music.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.music.playlist.MusicPlaylistEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicTracksFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicTracksFeedFilter.kt index 35bed93b10..f0de10a463 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicTracksFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/music/dal/MusicTracksFeedFilter.kt @@ -21,12 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.music.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.music.track.MusicTrackEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt index 5386f09d5a..bf71ccce1c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt @@ -82,6 +82,7 @@ import com.vitorpamplona.amethyst.commons.napplet.permissions.GrantState import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger import com.vitorpamplona.amethyst.commons.napplet.ui.PolicyCard import com.vitorpamplona.amethyst.commons.napplet.ui.symbol +import com.vitorpamplona.amethyst.commons.nip46RemoteSigner.ui.Nip46ActivityCard import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.cancel import com.vitorpamplona.amethyst.commons.resources.napplet_connected_app_capabilities @@ -122,7 +123,6 @@ import com.vitorpamplona.amethyst.napplet.descriptionRes import com.vitorpamplona.amethyst.napplet.labelRes import com.vitorpamplona.amethyst.napplet.resolveNappletMeta import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46ActivityCard import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46AppIcon import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46LiveStatus import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46ReconnectPill diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt index cddb8b18e9..f3bd69a5bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/NestsFeedLoaded.kt @@ -64,6 +64,10 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.navigation.routeFor +import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.EndedFlag +import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.LiveFlag +import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.PrivateFlag +import com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui.ScheduledFlag import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.live_stream_ended_tag import com.vitorpamplona.amethyst.commons.resources.nests_ended_ago @@ -99,10 +103,6 @@ import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.EndedFlag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveFlag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.PrivateFlag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ScheduledFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.LoadParticipants import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.dal.NestsFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.dal.NestsFeedFilter.NestBucket diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/dal/NestsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/dal/NestsFeedFilter.kt index 1b963d40fe..85a35951f6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/dal/NestsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/dal/NestsFeedFilter.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.ParticipantListBuilder import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -34,7 +35,6 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.community. import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.StatusTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt index 4297f1e73a..72a702b616 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestEditFieldRow.kt @@ -39,15 +39,15 @@ import com.vitorpamplona.amethyst.commons.chats.ui.ThinSendButton import com.vitorpamplona.amethyst.commons.nip30CustomEmojis.ui.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.reply_here +import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldBorder import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldModifier import com.vitorpamplona.amethyst.commons.ui.theme.EditFieldTrailingIconModifier import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightChat import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation -import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt index b72f99f4b1..1dded94988 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt @@ -36,6 +36,9 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.composer.DraftTagState +import com.vitorpamplona.amethyst.commons.model.composer.IExpiration +import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState @@ -44,6 +47,7 @@ import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_de import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events import com.vitorpamplona.amethyst.commons.resources.read_only_user import com.vitorpamplona.amethyst.commons.richtext.UrlParser +import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.text.currentWord import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor @@ -52,12 +56,8 @@ import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor -import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState -import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/SpeakerZapOverlay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/SpeakerZapOverlay.kt index 8e62c90521..0a2a12a512 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/SpeakerZapOverlay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/stage/SpeakerZapOverlay.kt @@ -46,9 +46,9 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange +import com.vitorpamplona.amethyst.commons.util.showAmountInteger import com.vitorpamplona.amethyst.commons.viewmodels.REACTION_WINDOW_SEC import com.vitorpamplona.amethyst.commons.viewmodels.RoomZap -import com.vitorpamplona.amethyst.ui.note.showAmountInteger import kotlinx.coroutines.delay /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt index a219448a67..09e15458df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt @@ -38,6 +38,7 @@ import com.vitorpamplona.amethyst.commons.service.BundledInsert import com.vitorpamplona.amethyst.commons.service.BundledUpdate import com.vitorpamplona.amethyst.commons.ui.notifications.Card import com.vitorpamplona.amethyst.commons.ui.notifications.CardFeedState +import com.vitorpamplona.amethyst.commons.util.equalImmutableLists import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.checkNotInMainThread diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt index bec6b7c1fa..9a64c999a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt @@ -52,6 +52,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.model.chats.formatHistoryReachDate import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.notification_feed_is_empty import com.vitorpamplona.amethyst.commons.resources.refresh @@ -82,7 +83,6 @@ import com.vitorpamplona.amethyst.ui.note.NutzapUserSetCompose import com.vitorpamplona.amethyst.ui.note.ZapUserSetCompose import com.vitorpamplona.amethyst.ui.note.types.ReplyRenderType import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.formatHistoryReachDate import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.donations.ShowDonationCard import kotlinx.coroutines.delay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/EqualImmutableLists.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/EqualImmutableLists.kt deleted file mode 100644 index e582bbe707..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/EqualImmutableLists.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications - -import kotlinx.collections.immutable.ImmutableList - -// Re-export from commons for backwards compatibility -fun <T> equalImmutableLists( - list1: ImmutableList<T>, - list2: ImmutableList<T>, -): Boolean = - com.vitorpamplona.amethyst.commons.util - .equalImmutableLists(list1, list2) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt index e1cc54cc4a..3eb9bfd2ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt @@ -29,9 +29,9 @@ import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.service.BundledInsert +import com.vitorpamplona.amethyst.commons.util.showAmountInteger import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.note.showCount import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/AmountValueFormatter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/AmountValueFormatter.kt index 70a2444ea8..71359a8851 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/AmountValueFormatter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/AmountValueFormatter.kt @@ -24,8 +24,8 @@ import androidx.compose.runtime.Stable import com.patrykandpatrick.vico.compose.cartesian.CartesianMeasuringContext import com.patrykandpatrick.vico.compose.cartesian.axis.Axis import com.patrykandpatrick.vico.compose.cartesian.data.CartesianValueFormatter -import com.vitorpamplona.amethyst.ui.note.showAmountIntegerWithZero -import com.vitorpamplona.amethyst.ui.note.showAmountWithZero +import com.vitorpamplona.amethyst.commons.util.showAmountIntegerWithZero +import com.vitorpamplona.amethyst.commons.util.showAmountWithZero import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.ShowDecimals @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt index 433d8d8183..0a79bfd44a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -31,7 +32,6 @@ import com.vitorpamplona.amethyst.commons.model.marmotGroups.MarmotGroupChatroom import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.buzz.jobs.JobErrorEvent import com.vitorpamplona.quartz.buzz.jobs.JobResultEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt index febe2b7ffe..f48d6ef95e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt @@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.commons.resources.messages_new_message_to import com.vitorpamplona.amethyst.commons.resources.messages_new_message_to_caption import com.vitorpamplona.amethyst.commons.resources.public_message import com.vitorpamplona.amethyst.commons.resources.zapraiser +import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar @@ -74,6 +75,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraise import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapToButton import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation import com.vitorpamplona.amethyst.commons.ui.text.onUiThread import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.Font14SP @@ -82,8 +84,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation -import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index 653df37778..32ae7d4e20 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -36,8 +36,13 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache +import com.vitorpamplona.amethyst.commons.model.composer.DraftTagState +import com.vitorpamplona.amethyst.commons.model.composer.IExpiration import com.vitorpamplona.amethyst.commons.model.composer.IZapRaiser +import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger +import com.vitorpamplona.amethyst.commons.model.composer.PreviewState import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder +import com.vitorpamplona.amethyst.commons.model.composer.toZapSplitSetup import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState @@ -45,6 +50,8 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details import com.vitorpamplona.amethyst.commons.resources.login_with_a_private_key_to_be_able_to_sign_events import com.vitorpamplona.amethyst.commons.resources.read_only_user +import com.vitorpamplona.amethyst.commons.service.upload.MediaUploadTracker +import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.IZapField @@ -56,18 +63,11 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger -import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing -import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState -import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber -import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewState import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState -import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.toZapSplitSetup import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/dal/PictureFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/dal/PictureFeedFilter.kt index 49aea905f5..39280f161a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/dal/PictureFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/dal/PictureFeedFilter.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.pictures.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip68Picture.PictureEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt index 5b8fdcda06..88abf9fe0a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/PodcastTopSupporters.kt @@ -54,11 +54,11 @@ import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.commons.ui.theme.Size16Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.ui.theme.grayText +import com.vitorpamplona.amethyst.commons.util.showAmountInteger import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay -import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt index dd7d2eaa9b..ee7b75f2ff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowScreen.kt @@ -73,10 +73,10 @@ import com.vitorpamplona.amethyst.commons.resources.podcast_show_type_episodic import com.vitorpamplona.amethyst.commons.resources.podcast_show_type_label import com.vitorpamplona.amethyst.commons.resources.podcast_show_type_serial import com.vitorpamplona.amethyst.commons.resources.podcast_show_website_label +import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SendingTopBar -import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.CoverImagePicker diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt index d6cfec4f68..ad55093d4a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/EditPodcastShowViewModel.kt @@ -26,10 +26,10 @@ import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.podcasts.V4VSplitEditorState +import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeScreen.kt index 07d1d263f8..3022b3717c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeScreen.kt @@ -89,11 +89,11 @@ import com.vitorpamplona.amethyst.commons.resources.podcast_episode_transcript_l import com.vitorpamplona.amethyst.commons.resources.podcast_episode_video_label import com.vitorpamplona.amethyst.commons.resources.podcast_new_episode import com.vitorpamplona.amethyst.commons.resources.podcast_publishing_banner +import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SendingTopBar import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelectSingle import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeViewModel.kt index 9f095c0afc..35090b62f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastEpisodeViewModel.kt @@ -27,10 +27,10 @@ import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.podcasts.V4VSplitEditorState +import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt index f33b87c771..8492d0c385 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerScreen.kt @@ -70,11 +70,11 @@ import com.vitorpamplona.amethyst.commons.resources.podcast_trailer_title_placeh import com.vitorpamplona.amethyst.commons.resources.podcast_trailer_upload_cta import com.vitorpamplona.amethyst.commons.resources.podcast_trailer_upload_hint import com.vitorpamplona.amethyst.commons.resources.podcast_trailer_url_label +import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SendingTopBar import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.ui.actions.StrippingFailureDialog import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.UploadInProgressBanner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt index af67cec9e6..304b497cb3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/authoring/NewPodcastTrailerViewModel.kt @@ -24,10 +24,10 @@ import android.content.Context import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName +import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator -import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nipXXPodcasting20.trailer.Podcasting20TrailerEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/PodcastEpisodesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/PodcastEpisodesFeedFilter.kt index b98f416758..4c816f5ece 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/PodcastEpisodesFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/PodcastEpisodesFeedFilter.kt @@ -21,12 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nipXXPodcasting20.episode.Podcasting20EpisodeEvent import com.vitorpamplona.quartz.podcasts.PodcastEpisode diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/PodcastsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/PodcastsFeedFilter.kt index 3026c56aa7..2092b73e6f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/PodcastsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/podcasts/dal/PodcastsFeedFilter.kt @@ -21,12 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nipF4Podcasts.metadata.PodcastMetadataEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/ClosedPollsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/ClosedPollsFeedFilter.kt index f076b195d0..f1a3215b9f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/ClosedPollsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/ClosedPollsFeedFilter.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/OpenPollsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/OpenPollsFeedFilter.kt index 9869ae301f..c4ff649bfe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/OpenPollsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/OpenPollsFeedFilter.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/PollsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/PollsFeedFilter.kt index 43294a5591..97af35782c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/PollsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/dal/PollsFeedFilter.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/results/PollResultsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/results/PollResultsScreen.kt index 884d0d827a..1ccf04c10c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/results/PollResultsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/results/PollResultsScreen.kt @@ -72,6 +72,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.navigation.routeFor +import com.vitorpamplona.amethyst.commons.relayClient.polls.results.RelayPollResponseLoader import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.poll_multiple_choice import com.vitorpamplona.amethyst.commons.resources.poll_results_all_options diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/dal/ProductsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/dal/ProductsFeedFilter.kt index bb061fb74b..70c485d6c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/dal/ProductsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/products/dal/ProductsFeedFilter.kt @@ -21,12 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.products.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent class ProductsFeedFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt index 0bc9808c07..861d97c490 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt @@ -81,6 +81,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.Size8dp +import com.vitorpamplona.amethyst.commons.viewmodels.UserExternalIdentitiesViewModel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -104,7 +105,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.hashtags.TabFollowe import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.ProfileHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.ProfileTopBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.identity.UserExternalIdentitiesViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.TabMutualConversations import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.dal.UserProfileMutualFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.TabNotesNewThreads diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/apps/dal/UserProfileAppsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/apps/dal/UserProfileAppsFeedFilter.kt index 0c285398e1..3264316f1b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/apps/dal/UserProfileAppsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/apps/dal/UserProfileAppsFeedFilter.kt @@ -21,12 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.apps.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.RootSiteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt index aef3178ea3..05cf74f316 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt @@ -21,13 +21,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt index 731c811b4d..ee6838f0a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt @@ -74,6 +74,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.SpacedBy5dp import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.util.toShortDisplay +import com.vitorpamplona.amethyst.commons.viewmodels.UserExternalIdentitiesViewModel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.util.LongPressCopyText @@ -84,7 +85,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.DisplayAppRecommendations import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.badges.DisplayBadges -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.identity.UserExternalIdentitiesViewModel import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt index 19e9a71e11..01cc8fc5b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt @@ -65,6 +65,7 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size100dp import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.ui.theme.userProfileBorderModifier +import com.vitorpamplona.amethyst.commons.viewmodels.UserExternalIdentitiesViewModel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataViewModel import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelectSingle @@ -72,7 +73,6 @@ import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.identity.UserExternalIdentitiesViewModel import kotlinx.coroutines.launch @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentContent.kt index 1bd3ac0ebb..8b98de44c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentContent.kt @@ -98,7 +98,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.commons.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.note.showAmount +import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt index 32bfc5a77a..0d2c6bdd90 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/payment/SendPaymentScreen.kt @@ -96,6 +96,7 @@ import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.model.DEFAULT_ONCHAIN_ZAP_SATS import com.vitorpamplona.amethyst.model.MIN_ONCHAIN_ZAP_SATS import com.vitorpamplona.amethyst.service.ClinkOfferPayer @@ -105,7 +106,6 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.payViaIntent -import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.nwc.nwcFailureDetail import com.vitorpamplona.amethyst.ui.nwc.nwcTimeoutMessage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt index 2b3399da6f..88214cac18 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt @@ -44,6 +44,8 @@ import com.vitorpamplona.amethyst.commons.resources.public_notif_section_explain import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.commons.viewmodels.MyRelayInfo +import com.vitorpamplona.amethyst.commons.viewmodels.RelayFeedViewModel import com.vitorpamplona.amethyst.ui.note.RelayCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt index 4fea0bc053..94a32f6e90 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt @@ -31,6 +31,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.viewmodels.RelayFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapNoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapNoteCompose.kt index f3d45d8565..fa36cb56dd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapNoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapNoteCompose.kt @@ -41,11 +41,11 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn +import com.vitorpamplona.amethyst.commons.util.showAmountInteger import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.UserActionOptions import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay -import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.ZapAmount diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt index 3e16e0f612..186ea85888 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt @@ -27,7 +27,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.zaps import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.ui.note.showAmountInteger +import com.vitorpamplona.amethyst.commons.util.showAmountInteger import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/dal/PublicChatsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/dal/PublicChatsFeedFilter.kt index d30cb73aa2..ac88e54778 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/dal/PublicChatsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/dal/PublicChatsFeedFilter.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.publicChats.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/BarcodeDecoder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/BarcodeDecoder.kt index 67f6a03ee5..5a3b037399 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/BarcodeDecoder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/BarcodeDecoder.kt @@ -23,6 +23,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.scanner import android.graphics.Bitmap import android.graphics.Rect import androidx.camera.core.ImageProxy +import com.vitorpamplona.amethyst.commons.qrcode.ScanBounds +import com.vitorpamplona.amethyst.commons.qrcode.ScanPoint +import com.vitorpamplona.amethyst.commons.qrcode.ScanResult import com.vitorpamplona.quartz.utils.Log import zxingcpp.BarcodeReader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrFrameAnalyzer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrFrameAnalyzer.kt index d526f5e914..f883f31aaf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrFrameAnalyzer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrFrameAnalyzer.kt @@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.scanner import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageProxy +import com.vitorpamplona.amethyst.commons.qrcode.ScanFrame +import com.vitorpamplona.amethyst.commons.qrcode.ScanResult import com.vitorpamplona.quartz.utils.Log import kotlin.math.max diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrImageImport.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrImageImport.kt index 83141ac20c..d2a1051a7e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrImageImport.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrImageImport.kt @@ -26,6 +26,7 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import androidx.core.graphics.scale +import com.vitorpamplona.amethyst.commons.qrcode.ScanResult import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ensureActive diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerDialog.kt index f6efc9f418..e05e6246cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerDialog.kt @@ -90,6 +90,7 @@ import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.shouldShowRationale +import com.vitorpamplona.amethyst.commons.qrcode.ScanResult import com.vitorpamplona.amethyst.commons.qrcode.classifyScannedPayload import com.vitorpamplona.amethyst.commons.qrcode.ui.QrImageCodeChooser import com.vitorpamplona.amethyst.commons.qrcode.ui.ScanOutcomeSheet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerOverlay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerOverlay.kt index 5de6999b6b..18b8bccd6e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerOverlay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerOverlay.kt @@ -53,6 +53,9 @@ import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.qrcode.ScanBounds +import com.vitorpamplona.amethyst.commons.qrcode.ScanFrame +import com.vitorpamplona.amethyst.commons.qrcode.ScanPoint import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.close import com.vitorpamplona.amethyst.commons.resources.qr_scanner_dark_hint diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerState.kt index 4a3de54979..01c204e033 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerState.kt @@ -25,7 +25,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import com.vitorpamplona.amethyst.commons.qrcode.ScanFrame +import com.vitorpamplona.amethyst.commons.qrcode.ScanResult import com.vitorpamplona.amethyst.commons.qrcode.ScannedPayload +import com.vitorpamplona.amethyst.commons.qrcode.StructuredAppendAccumulator /** * Everything the scanner UI draws, and the decision of what a frame means. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt index d2e0a0496f..67146fe09a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/subscriptions/ActiveSubscriptionsScreen.kt @@ -71,6 +71,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.Aut import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilter import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurposeGroup +import com.vitorpamplona.amethyst.commons.relays.ui.SubPurposeLabels import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.active_subs_filters import com.vitorpamplona.amethyst.commons.resources.active_subs_groups @@ -102,7 +103,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.loadMarmotRelayIcon import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.rememberMarmotGroupAvatarUrl import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord.rememberConcordImageModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.SubPurposeLabels import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt index 6f4c0a13ad..bb68210b82 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt @@ -66,6 +66,10 @@ import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SettingsSearchTopBar +import com.vitorpamplona.amethyst.commons.ui.settings.SettingsCategory +import com.vitorpamplona.amethyst.commons.ui.settings.SettingsEntry +import com.vitorpamplona.amethyst.commons.ui.settings.SettingsIcon +import com.vitorpamplona.amethyst.commons.ui.settings.filterSettings import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HiddenWordsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HiddenWordsScreen.kt index 81edb6d1d7..3a2207444f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HiddenWordsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HiddenWordsScreen.kt @@ -60,6 +60,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.commons.feeds.StringFeedState import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.hidden_words import com.vitorpamplona.amethyst.commons.resources.hide_new_word_label diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt index 8597f78173..2e091a4cf1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt @@ -52,6 +52,7 @@ import com.vitorpamplona.amethyst.commons.resources.payment_targets_section_expl import com.vitorpamplona.amethyst.commons.resources.wallet_connect import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SavingTopBar +import com.vitorpamplona.amethyst.commons.ui.settings.SettingsCategory import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.SettingsCategorySpacingModifier import com.vitorpamplona.amethyst.commons.ui.theme.grayText diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NotificationSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NotificationSettingsScreen.kt index 02f18f92e9..3c4e4be86f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NotificationSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NotificationSettingsScreen.kt @@ -88,6 +88,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackBu import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn +import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.service.notifications.BatteryOptimizationHelper import com.vitorpamplona.amethyst.service.notifications.NotificationChannels @@ -95,7 +96,6 @@ import com.vitorpamplona.amethyst.ui.components.PushNotificationProviderTile import com.vitorpamplona.amethyst.ui.components.hasPushNotificationProvider import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay -import com.vitorpamplona.amethyst.ui.note.toShortDisplay 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityListsCommon.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityListsCommon.kt index 790a12547b..1e5368f749 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityListsCommon.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityListsCommon.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.feeds.UserFeedState import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.navigation.routeFor @@ -75,7 +76,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay -import com.vitorpamplona.amethyst.ui.screen.UserFeedState import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import org.jetbrains.compose.resources.StringResource diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt index a180a43368..ff9348aea5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogBuilder.kt @@ -106,6 +106,9 @@ import com.vitorpamplona.amethyst.commons.resources.video_player_settings import com.vitorpamplona.amethyst.commons.resources.zaps import com.vitorpamplona.amethyst.commons.resources.zaps_search_keywords import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.settings.SettingsCategory +import com.vitorpamplona.amethyst.commons.ui.settings.SettingsEntry +import com.vitorpamplona.amethyst.commons.ui.settings.SettingsIcon import org.jetbrains.compose.resources.StringResource /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedView.kt index 99db311d0f..96b549e38d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedView.kt @@ -35,6 +35,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.feeds.StringFeedState import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.feed_is_empty import com.vitorpamplona.amethyst.commons.resources.refresh diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt index a4e4693d5e..b4a28274ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt @@ -27,10 +27,11 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.commons.feeds.FeedFilter import com.vitorpamplona.amethyst.commons.feeds.InvalidatableContent +import com.vitorpamplona.amethyst.commons.feeds.StringFeedState import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.service.BundledUpdate +import com.vitorpamplona.amethyst.commons.util.equalImmutableLists import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.quartz.utils.Log import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnBackupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnBackupScreen.kt index 0876d4cf5d..762afc71a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnBackupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnBackupScreen.kt @@ -52,6 +52,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.cordn.ui.BusyLabel +import com.vitorpamplona.amethyst.commons.cordn.ui.SettingsFormBlock import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.cancel import com.vitorpamplona.amethyst.commons.resources.cordn_backup_contents_body diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt index cdae46aa2a..2dc743c32a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt @@ -62,6 +62,7 @@ import com.vitorpamplona.amethyst.commons.cordn.CoordinatorConfig import com.vitorpamplona.amethyst.commons.cordn.CoordinatorHealth import com.vitorpamplona.amethyst.commons.cordn.CordnCoordinatorDiscovery import com.vitorpamplona.amethyst.commons.cordn.DiscoveredCoordinator +import com.vitorpamplona.amethyst.commons.cordn.ui.SettingsFormBlock import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.cache.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnKeyPackagesScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnKeyPackagesScreen.kt index b3ebf82863..7d01ae4159 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnKeyPackagesScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnKeyPackagesScreen.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.cordn.CoordinatorConfig +import com.vitorpamplona.amethyst.commons.cordn.ui.SettingsFormBlock import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.cordn_coordinators_none import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnLinkScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnLinkScreen.kt index f4ac911967..2f793891ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnLinkScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnLinkScreen.kt @@ -47,6 +47,7 @@ import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.cordn.CordnLinkInspection import com.vitorpamplona.amethyst.commons.cordn.ui.CordnExposureCard +import com.vitorpamplona.amethyst.commons.cordn.ui.SettingsFormBlock import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.cordn_link_clear import com.vitorpamplona.amethyst.commons.resources.cordn_link_coordinator diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnMigrateScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnMigrateScreen.kt index 6c55c2bbb0..7f5ca299c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnMigrateScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnMigrateScreen.kt @@ -48,6 +48,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.cordn.CordnMigration +import com.vitorpamplona.amethyst.commons.cordn.ui.BusyLabel +import com.vitorpamplona.amethyst.commons.cordn.ui.SettingsFormBlock import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable import com.vitorpamplona.amethyst.commons.resources.cordn_group_unavailable_detail diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt index 1ce01265b3..df18f48a12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt @@ -83,6 +83,7 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.nip46Signer.Nip46ActivityEntry +import com.vitorpamplona.amethyst.commons.nip46RemoteSigner.ui.Nip46ActivityCard import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.nip46_signer_activity_title import com.vitorpamplona.amethyst.commons.resources.nip46_signer_background_hint diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/dal/ShortsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/dal/ShortsFeedFilter.kt index dfbaf15534..02aacf30d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/dal/ShortsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/dal/ShortsFeedFilter.kt @@ -21,6 +21,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.shorts.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams +import com.vitorpamplona.amethyst.commons.feeds.SupportedContent import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -29,9 +31,7 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.commons.relayClient.video.SUPPORTED_VIDEO_FEED_MIME_TYPES_SET import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder -import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal.SupportedContent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip71Video.AddressableShortVideoEvent import com.vitorpamplona.quartz.nip71Video.VideoMeta diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/dal/SoftwareAppsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/dal/SoftwareAppsFeedFilter.kt index 0f3bb8cdf1..a2cdbb0f7c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/dal/SoftwareAppsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/softwareapps/dal/SoftwareAppsFeedFilter.kt @@ -21,12 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.softwareapps.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.filterIntoSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.application.SoftwareApplicationEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 510b334ad9..5bbc3dfe06 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -125,6 +125,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.lessImportantLink import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.selectedNote import com.vitorpamplona.amethyst.commons.ui.thread.drawReplyLevel +import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.commons.viewmodels.thread.LevelFeedViewModel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeCommunityApprovalNeedStatus import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid @@ -168,7 +169,6 @@ import com.vitorpamplona.amethyst.ui.note.elements.Reward import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation import com.vitorpamplona.amethyst.ui.note.nip22Comments.DisplayCommentScope import com.vitorpamplona.amethyst.ui.note.observeEdits -import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.note.types.AudioHeader import com.vitorpamplona.amethyst.ui.note.types.AudioTrackHeader import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlScreen.kt index 47784e4175..e8946326b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/url/UrlScreen.kt @@ -30,6 +30,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.external_content_title +import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewCard import com.vitorpamplona.amethyst.commons.ui.components.UrlPreviewState import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded @@ -37,7 +38,6 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.MaxWidthWithHorzPadding -import com.vitorpamplona.amethyst.ui.components.UrlPreviewCard import com.vitorpamplona.amethyst.ui.components.rememberUrlPreviewState import com.vitorpamplona.amethyst.ui.feeds.FeedLoaded import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt index 7fad0d489e..42dd92faf4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt @@ -21,6 +21,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams +import com.vitorpamplona.amethyst.commons.feeds.SupportedContent import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache @@ -29,7 +31,6 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.commons.relayClient.video.SUPPORTED_VIDEO_FEED_MIME_TYPES_SET import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestrator.kt index 7b3419010c..7a42bf22e3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestrator.kt @@ -30,6 +30,7 @@ import com.davotoula.lightcompressor.hls.HlsUploaded import com.davotoula.lightcompressor.hls.Rendition import com.davotoula.lightcompressor.hls.SimpleHlsListener import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName +import com.vitorpamplona.amethyst.commons.service.upload.HlsPublishState import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult import com.vitorpamplona.amethyst.service.uploads.hls.HlsBlobUploader import com.vitorpamplona.amethyst.service.uploads.hls.HlsKind1SiblingBuilder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestratorFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestratorFactory.kt index d84708ff46..610ce5be46 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestratorFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestratorFactory.kt @@ -26,6 +26,7 @@ import android.media.MediaMetadataRetriever import android.net.Uri import com.davotoula.lightcompressor.hls.HlsContentTypes import com.davotoula.lightcompressor.hls.HlsUploadHelper +import com.vitorpamplona.amethyst.commons.service.upload.HlsPublishState import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult import com.vitorpamplona.amethyst.service.uploads.PreviewMetadataCalculator diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoScreen.kt index fe04b878af..2c4cb16da7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoScreen.kt @@ -110,6 +110,7 @@ import com.vitorpamplona.amethyst.commons.resources.hls_title_label import com.vitorpamplona.amethyst.commons.resources.hls_title_placeholder import com.vitorpamplona.amethyst.commons.resources.hls_try_again import com.vitorpamplona.amethyst.commons.resources.share_hls_video +import com.vitorpamplona.amethyst.commons.service.upload.HlsPublishState import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoViewModel.kt index 1bf8029983..4a324037c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/NewHlsVideoViewModel.kt @@ -33,6 +33,7 @@ import com.davotoula.lightcompressor.hls.HlsLadder import com.davotoula.lightcompressor.utils.CompressorUtils import com.vitorpamplona.amethyst.commons.model.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName +import com.vitorpamplona.amethyst.commons.service.upload.HlsPublishState import com.vitorpamplona.amethyst.model.Account import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt index 131305e7bb..a2ea4d577a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt @@ -120,13 +120,13 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.bitcoinColor +import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.model.DEFAULT_ONCHAIN_ZAP_SATS import com.vitorpamplona.amethyst.model.MIN_ONCHAIN_ZAP_SATS import com.vitorpamplona.amethyst.ui.components.namecoin.NamecoinResolutionRow import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState -import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/ReloadMintScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/ReloadMintScreen.kt index cef6a7c1bd..d6a85fe7e3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/ReloadMintScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/ReloadMintScreen.kt @@ -98,8 +98,8 @@ import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText +import com.vitorpamplona.amethyst.commons.util.showAmount import com.vitorpamplona.amethyst.ui.note.UserPicture -import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.launch import java.util.UUID diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt index e6d0cc127a..452a991874 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt @@ -73,6 +73,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.commons.clink.ui.ClinkBudgetDialog import com.vitorpamplona.amethyst.commons.hashtags.Cashu import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons import com.vitorpamplona.amethyst.commons.icons.symbols.Icon diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt index b01f4380c0..1934a77dcc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt @@ -59,6 +59,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.navigation.Route +import com.vitorpamplona.amethyst.commons.model.nip47WalletConnect.TransactionRowLabels import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.back import com.vitorpamplona.amethyst.commons.resources.wallet_filter_all diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/wizard/CashuWalletWizardViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/wizard/CashuWalletWizardViewModel.kt index 9051c9a277..f8c14aae0e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/wizard/CashuWalletWizardViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/wizard/CashuWalletWizardViewModel.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.wizard import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.cashu.CashuWalletDiscovery import com.vitorpamplona.amethyst.commons.cashu.ops.describeMintError import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.nip60Cashu.CashuWalletState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/dal/WorkoutFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/dal/WorkoutFeedFilter.kt index 48397e0c3f..127553191b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/dal/WorkoutFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/dal/WorkoutFeedFilter.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.dal import com.vitorpamplona.amethyst.commons.feeds.AdditiveFeedFilter +import com.vitorpamplona.amethyst.commons.feeds.FilterByListParams import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.topNavFeeds.TopFilter import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder import com.vitorpamplona.quartz.experimental.fitness.workout.WorkoutRecordEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginScreen.kt index fcc9a24726..b35647c0e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginScreen.kt @@ -67,6 +67,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.account.ui.login.LoginButton +import com.vitorpamplona.amethyst.commons.account.ui.login.LoginErrorManager import com.vitorpamplona.amethyst.commons.account.ui.login.SignUpButton import com.vitorpamplona.amethyst.commons.hashtags.Amethyst import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel.kt index 38e6c7ea99..b1062bd9c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.BuildConfig +import com.vitorpamplona.amethyst.commons.account.ui.login.LoginErrorManager import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.invalid_key import com.vitorpamplona.amethyst.commons.resources.invalid_key_with_message diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpScreen.kt index 4425f0a89b..eb80fa22ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpScreen.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.account.ui.login.LoginErrorManager import com.vitorpamplona.amethyst.commons.account.ui.signup.LoginButton import com.vitorpamplona.amethyst.commons.account.ui.signup.SignUpButton import com.vitorpamplona.amethyst.commons.hashtags.Amethyst @@ -70,7 +71,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.loggedOff.TorSettingsSetup import com.vitorpamplona.amethyst.ui.screen.loggedOff.legal.TermsGate -import com.vitorpamplona.amethyst.ui.screen.loggedOff.login.LoginErrorManager import kotlinx.coroutines.launch @Preview(device = "spec:width=2160px,height=2340px,dpi=440") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpViewModel.kt index 42307cb17b..5e2b8b3c09 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpViewModel.kt @@ -27,11 +27,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.BuildConfig +import com.vitorpamplona.amethyst.commons.account.ui.login.LoginErrorManager import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.name_is_required import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager -import com.vitorpamplona.amethyst.ui.screen.loggedOff.login.LoginErrorManager @Stable class SignUpViewModel : ViewModel() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt index 415d54252d..01ef24d512 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt @@ -77,6 +77,7 @@ import com.vitorpamplona.amethyst.commons.resources.tor_use_videos import com.vitorpamplona.amethyst.commons.resources.tor_use_videos_explainer import com.vitorpamplona.amethyst.commons.resources.use_internal_tor import com.vitorpamplona.amethyst.commons.resources.use_internal_tor_explainer +import com.vitorpamplona.amethyst.commons.tor.TorDialogViewModel import com.vitorpamplona.amethyst.commons.tor.TorPresetType import com.vitorpamplona.amethyst.commons.tor.TorSettings import com.vitorpamplona.amethyst.commons.tor.TorType diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/LegalSettingsSection.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/LegalSettingsSection.kt index 03ffe47da7..68ff8804ca 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/LegalSettingsSection.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/LegalSettingsSection.kt @@ -28,6 +28,9 @@ import com.vitorpamplona.amethyst.commons.resources.child_safety_search_keywords import com.vitorpamplona.amethyst.commons.resources.child_safety_standards import com.vitorpamplona.amethyst.commons.resources.privacy_policy import com.vitorpamplona.amethyst.commons.resources.privacy_policy_search_keywords +import com.vitorpamplona.amethyst.commons.ui.settings.SettingsCategory +import com.vitorpamplona.amethyst.commons.ui.settings.SettingsEntry +import com.vitorpamplona.amethyst.commons.ui.settings.SettingsIcon /** Play build surfaces the GitHub-hosted legal policy links. */ fun legalSettingsCategory(uriHandler: UriHandler): SettingsCategory? = diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelTest.kt index 1358a9f67d..f70a77734c 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/calendar/CalendarsViewModelTest.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.calendar import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStore import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsViewMode +import com.vitorpamplona.amethyst.commons.nip52Calendar.ui.CalendarsViewMode import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.startOfWeek import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsPublishOrchestratorTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsPublishOrchestratorTest.kt index 778232794a..2d02dd8886 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsPublishOrchestratorTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/uploads/hls/HlsPublishOrchestratorTest.kt @@ -29,11 +29,11 @@ import com.davotoula.lightcompressor.hls.HlsUploaded import com.davotoula.lightcompressor.hls.Rendition import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType +import com.vitorpamplona.amethyst.commons.service.upload.HlsPublishState import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.hls.HlsPosterUpload import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.hls.HlsPublishOrchestrator import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.hls.HlsPublishRequest -import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.hls.HlsPublishState import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import kotlinx.coroutines.flow.MutableStateFlow diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerStateTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerStateTest.kt index df79de8085..c2af5a01dc 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerStateTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerStateTest.kt @@ -20,6 +20,9 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.scanner +import com.vitorpamplona.amethyst.commons.qrcode.ScanFrame +import com.vitorpamplona.amethyst.commons.qrcode.ScanResult +import com.vitorpamplona.amethyst.commons.qrcode.StructuredAppendAccumulator import com.vitorpamplona.amethyst.commons.qrcode.classifyScannedPayload import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModelKeyKindTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModelKeyKindTest.kt index 814a5b1ba2..5f83740441 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModelKeyKindTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModelKeyKindTest.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedOff.login import androidx.compose.ui.text.input.TextFieldValue +import com.vitorpamplona.amethyst.commons.account.ui.login.LoginErrorManager import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.login_bunker_not_supported import com.vitorpamplona.amethyst.commons.resources.login_nostrconnect_not_supported diff --git a/commons/ARCHITECTURE.md b/commons/ARCHITECTURE.md index 8f44e28665..7fd1105b31 100644 --- a/commons/ARCHITECTURE.md +++ b/commons/ARCHITECTURE.md @@ -132,7 +132,7 @@ type (`LazyListState`, `TextFieldValue`, `TextFieldState`) goes to `commonsUI`. ### UI (Compose — lives in **`commonsUI`**) | Package | UI? | Purpose | |----------------|-----|---------| -| `ui` | yes | **Cross-cutting** shared composables only, organized by area: `ui/components`, `ui/theme`, `ui/signing`, `ui/thread`, `ui/note`, `ui/richtext`, `ui/search`, `ui/notifications`, `ui/screens`, `ui/layouts`, `ui/feeds` (feed shell: empty/error/loading states, refresh box, remembered scroll states), `ui/navigation` (`INav`, `EmptyNav`, top bars, drawer swipe), `ui/markdown`, `ui/privacylock`, plus Compose helpers in `ui/state` (cached-state) and `ui/text` (TextField extensions). Feature-specific UI lives in `<feature>/ui`, **not** here. Nothing under `ui.*` lives in *this* module any more: the feed DAL that used to sit in `ui/feeds` is now `feeds/`, and the reply-context logic that sat in `ui/note` (`replyingDirectlyTo`, `ReplyContext`) is now in `model/` next to `ThreadAssembler`. | +| `ui` | yes | **Cross-cutting** shared composables only, organized by area: `ui/components`, `ui/theme`, `ui/signing`, `ui/thread`, `ui/note`, `ui/richtext`, `ui/search`, `ui/notifications`, `ui/screens`, `ui/layouts`, `ui/feeds` (feed shell: empty/error/loading states, refresh box, remembered scroll states), `ui/navigation` (`INav`, `EmptyNav`, `ObservableNav`/`TwoPaneNav`/`ShareToDMNav`, top bars, drawer swipe), `ui/settings` (the searchable settings catalog), `ui/markdown`, `ui/privacylock`, plus Compose helpers in `ui/state` (cached-state) and `ui/text` (TextField extensions). Feature-specific UI lives in `<feature>/ui`, **not** here. Nothing under `ui.*` lives in *this* module any more: the feed DAL that used to sit in `ui/feeds` is now `feeds/`, and the reply-context logic that sat in `ui/note` (`replyingDirectlyTo`, `ReplyContext`) is now in `model/` next to `ThreadAssembler`. | | `nip23LongContent` | yes | Long-form (NIP-23) article UI: `nip23LongContent/ui/article` (reader) + `…/ui/editor` (authoring). The model lives in `model/nip23LongContent` (here). | | `icons` | yes | `ImageVector` icon definitions + builders, Material Symbols codepoints, the icon-font glyph tables. | | `hashtags` | yes | Custom hashtag `ImageVector`s. | @@ -142,7 +142,7 @@ type (`LazyListState`, `TextFieldValue`, `TextFieldState`) goes to `commonsUI`. | `napplet` | mixed | Protocol/permission logic here; `NappletWebContract` (serves the shell/shim from `composeResources`) in `commonsUI`. | | `favorites`, `nip30CustomEmojis`, `nip34Git`, `nip85TrustedAssertions`, `nip53LiveActivities` | mixed | Logic here; each feature's `ui/` (or the flat `FavoriteAppIcon`, `EmojiSuggestionState`) in `commonsUI`. | | `chats` | yes | Composables shared by every chat kind (DMs, public chats, relay groups, concord): unread badge, divisors, system messages, author line, send button, reply toggle, new-conversation screen. Chat *models* are in `model/chats`. | -| `nip17Dm`, `nip23LongContent`, `nip28PublicChat`, `nip29RelayGroups`, `nip32Labeling`, `nip43RelayMembers`, `nip51Lists`, `nip52Calendar`, `nip56Reports`, `nip72ModCommunities`, `nip73ExternalIds`, `nip99Classifieds`, `nipC0CodeSnippets`, `nipCCGeocaching`, `birdstar`, `nipsOnNostr`, `buzz`, `cashu`, `concord`, `music`, `mediaServers`, `browser`, `profile`, `napplet` | yes / mixed | Single-feature UI under `<feature>/ui` (named after the `quartz` package, or its concern name when `quartz` has none). Where the package also has logic (`cashu`, `browser`, `napplet`, `profile`) that part stays here; the `buzz` and `concord` models are in `model/buzz` and `model/concord`. | +| `nip17Dm`, `nip23LongContent`, `nip28PublicChat`, `nip29RelayGroups`, `nip32Labeling`, `nip43RelayMembers`, `nip51Lists`, `nip52Calendar`, `nip56Reports`, `nip72ModCommunities`, `nip73ExternalIds`, `nip99Classifieds`, `nipC0CodeSnippets`, `nipCCGeocaching`, `birdstar`, `nipsOnNostr`, `buzz`, `cashu`, `clink`, `concord`, `music`, `mediaServers`, `nip46RemoteSigner`, `browser`, `profile`, `napplet` | yes / mixed | Single-feature UI under `<feature>/ui` (named after the `quartz` package, or its concern name when `quartz` has none). Where the package also has logic (`cashu`, `browser`, `napplet`, `profile`) that part stays here; the `buzz` and `concord` models are in `model/buzz` and `model/concord`. | ### Mixed (documented debt — see §4) | Package | UI? | Purpose | diff --git a/commons/plans/2026-08-30-commons-migration-sweep.md b/commons/plans/2026-08-30-commons-migration-sweep.md index 5e0e2d40ef..c67ba3f177 100644 --- a/commons/plans/2026-08-30-commons-migration-sweep.md +++ b/commons/plans/2026-08-30-commons-migration-sweep.md @@ -1553,3 +1553,136 @@ A transitive-blocker sweep of the 1,385 files under `amethyst/…/ui/`, after th same gap. - Adding the module costs about +28 MB to the unpacked runtime. That is a packaging call, left for the Desktop phase. + +## 2026-09-27 (later) — the unblocked `ui/` files, and one chess view model + +### How the list was rebuilt + +The morning's "80 files have no blocker" count was not saved, so it was recomputed +with a script (not committed; it is a one-off) that: + +- indexes every public top-level declaration in `amethyst/src/{main,play,fdroid}`; +- follows each file's imports **and** its same-package references (the + under-count this plan's header warns about); +- marks a file blocked by `android.*`, a non-KMP `androidx.*`, `R.`/`BuildConfig`, + or a library the shared modules lack; +- takes the fixpoint, then assigns each survivor a module and source set from what it + (transitively) uses: Compose UI, `Res` or a `commonsUI` symbol means `commonsUI`; + `java.*`/JVM tokens or a jvmAndroid-only `commons` symbol means `jvmAndroid`. + +It found **96** such files under `ui/` (142 app-wide). 68 of the `ui/` ones moved +(plus `service/uploads/SuspendableConfirmation`, which one of them needs), 3 were app +copies of existing `commons` code and were deleted, and 25 stay, for the reasons below. + +### Moved + +**To `commons`** + +| Package | Files | +|---|---| +| `model` | `ConcordLastRead`, `MarmotGroupLastRead`, `RelayGroupLastRead` (beside `privateChatLastReadRoute`) | +| `model/chats` | `ConcordServerRoomNote`, `RelayGroupServerRoomNote`, `HistoryDateFormat` (jvmAndroid) | +| `model/composer` | `NewMessageTagger`, `DraftTagState`, `IExpiration`, `SplitConversor`, `PreviewState` (jvmAndroid) | +| `model/navigation` | `RouteTextArgs`, `ShareToDMRouteRewriter` | +| `model/buzz` | `AgentWork` | +| `model/nip29RelayGroups` | `GroupDiscoveryConstraintResolver` (was `dal/GroupDiscoveryConstraint.kt`; renamed because the matcher it resolves to already owns that file name here) | +| `model/nip47WalletConnect` | `TransactionRowLabels` | +| `model/nip72Communities` | `CommunityRulesLookup` | +| `model/nip92IMeta` | `ImetaContent` | +| `relayClient/chatrooms` (new) | the five `Filter*` functions of the chatroom-list assembler | +| `relayClient/channel/relayGroup` | `RelayGroupOpenThreadsFilterAssembler`, `RelayGroupsDiscoveryFilter` | +| `relayClient/hashtag` (new), `relayClient/geocaches`, `relayClient/polls/results` | `FilterPostsByHashtags`, `GeocacheListingKinds`, `RelayPollResponseLoader` | +| `feeds` | `FilterByListParams`, `UserFeedState`, `StringFeedState`, `SupportedContent` | +| `viewmodels` | `RelayFeedViewModel`, `UserExternalIdentitiesViewModel` | +| `service/upload` | `MediaUploadTracker`, `HlsPublishState`, `SuspendableConfirmation` | +| `audio`, `music`, `qrcode`, `cashu`, `mediaServers`, `tor` | `PitchShifter`; `MusicFormatting`; `ScanResult` + `StructuredAppendAccumulator`; `CashuWalletDiscovery` (jvmAndroid); `MediaServerHealth` (jvmAndroid); `TorDialogViewModel` | +| `util` | `DebouncedPublisher`, `TimeFormatUtils` + `ZapFormatterNoDecimals` (jvmAndroid) | + +**To `commonsUI`** + +| Package | Files | +|---|---| +| `ui/components` | `InformationDialog`, `UrlPreviewCard` (both jvmAndroid) | +| `ui/text` | `MentionPreservingInputTransformation` (its `MENTION_REGEX` is now public; the app's `UrlUserTagOutputTransformation` shares it) | +| `ui/navigation/navs` | `ObservableNav`, `TwoPaneNav`, `ShareToDMNav` | +| `ui/navigation/bottombars`, `ui/insets`, `ui/theme`, `ui/settings` (new) | `TabReselectCoordinator`; `KeyboardState`; `ColorSchemePreview` (jvmAndroid: it formats with `String.format`); `SettingsCatalog` | +| feature `ui/` packages | `account/ui/login/LoginErrorManager`, `clink/ui/ClinkBudgetDialog`, `cordn/ui/{BusyLabel,SettingsFormBlock}`, `nip46RemoteSigner/ui/Nip46ActivityUi`, `nip52Calendar/ui/CalendarsViewMode`, `nip53LiveActivities/ui/StreamingStatusFlags`, `nip72ModCommunities/ui/CommunityRulesViolationBanner`, `relays/ui/SubPurposeLabels`, `service/upload/ui/StrippingFailureDialog` | +| `audio` | `VoicePreset`, `RecordingIndicators` (jvmAndroid) | + +Most files moved unchanged apart from the package line. The exceptions, all for +`commonMain`: + +- `formatTrackDuration` went from `internal` to public so the app's music renderers can + still call it. Its `"%d:%02d".format(...)` is now a `padStart`, because `String.format` + is JVM-only. +- `RouteTextArgs`: `codePointAt` / `Character.charCount` → the commons + `codePointAtKmp` / `codePointCharCount`, which mirror the JDK ones. +- `TorDialogViewModel`: `Integer.parseInt` → `toInt()`. Both throw + `NumberFormatException` on the JVM. +- `UserExternalIdentitiesViewModel.Factory`: `create(Class<T>)` → the multiplatform + `create(KClass<T>, CreationExtras)`, as `PollResultsViewModel` already does. +- `Dispatchers.IO` needs `import kotlinx.coroutines.IO` in common code, and + `RelayFeedViewModel`'s `javaClass.simpleName` log is now `this::class.simpleName`. + +A tooling note for the next sweep: stripping comments with a regex before scanning +identifiers breaks on a `/*` inside a string (`"image/*"`), which swallows code up to the +next `*/`. Tokenize strings and comments together. And scan bodies, not only imports, +for inline `android.`/`java.` names and `javaClass`. + +Eleven pure unit tests moved with their code to `commons` jvmTest (NewMessageTagger key +parsing, PitchShifter, RouteTextArgs, ShareToDMRouteRewriter, DebouncedPublisher, +RelayGroupDiscoveryConstraint, StructuredAppendAccumulator, SupportedContent, +TransactionRowLabels, CommunityRulesLookup) and `commonsUI` jvmTest +(SettingsCatalogFilter). + +**Deleted app copies** (callers repointed at the `commons` original): +`ui/note/PubKeyFormatter.kt` (the commons `toShortDisplay` is the same with an optional +`prefixSize`), `ui/note/ZapFormatter.kt` (same functions; the commons `TenGiga`…`OneKilo` +constants became public for `ZapFormatterNoDecimals`), and the +`EqualImmutableLists.kt` re-export shim. + +### `ChessViewModelNew` and `DesktopChessViewModelNew` → one `ChessViewModel` + +The two classes forwarded the same ~40 members to `ChessLobbyLogic`; only the +adapters and the scope differed. `commons/nip64Chess/ChessViewModel` now takes the +adapters, the polling config, the dismissed-games store and an optional scope: + +- **Android:** `ChessViewModelFactory` builds it with the Android adapters and no + scope, so it runs on `viewModelScope` and stops polling in `onCleared`, as before. + The factory lost its unused `Application` parameter. The `viewModel(key = …)` key + changed from `ChessViewModelNew-<pubkey>` to `ChessViewModel-<pubkey>`, in all four + call sites together, so the lobby, game screen, note card and home button still share + one instance. +- **Desktop:** `ChessScreen` builds it with the Desktop adapters and its + `rememberCoroutineScope()`, which is what `DesktopChessViewModelNew` did. The + `UserMetadataCache` that class owned is now remembered next to it in `ChessScreen`. + The debug `instanceId` (`System.identityHashCode`) went, since it isn't KMP. + +### Left in the app on purpose (unblocked, but not shared code) + +- `ui/screen/AndroidFeedViewModel`, `threadview/dal/AndroidLevelFeedViewModel`: they + exist to bind Android's process-wide `LocalCache`. Desktop still runs + `DesktopLocalCache` (Wave 2 part B). +- `ui/navigation/ShareIntentRouting`: names the manifest's `<activity-alias>`es. +- `ui/screen/loggedIn/embed/*` (9 files) and `browser/EmbeddedPageRequests`: bridges + for the Android WebView host. No other front end embeds a WebView. +- `workouts/health/HealthConnectRationaleScreen`: Health Connect is Android-only. +- `ui/tor/{TorManager,TorBackend,ArtiNative,ArtiGuardState,TorConnectionFailureDialog}`: + the in-process Arti client. Desktop drives an external Tor. +- `ui/tor/TorServiceStatus`: `commons/tor` has its own copy that differs on purpose + (Android has `Bootstrapping`, Desktop has `Error`). Merging them changes every `when` + over it on both platforms; that's a design step, not a move. +- `ui/actions/UrlUserTagOutputTransformation`: calls `android.util.Patterns.WEB_URL` + by its fully-qualified name, which an import-based scan cannot see. It needs a KMP URL + matcher (commons `richtext` has one) before it can move. +- `nests/room/{lifecycle/NestRoomEventCollectors, participants/RoomParticipantActions, + stage/SpeakerReactionOverlay, stage/SpeakerZapOverlay}`: `internal` pieces of the + Android audio-room screen, which Desktop doesn't have. + +### Unblocked outside `ui/` (not moved in this round) + +The same sweep finds 46 more movable files outside `ui/`. The notable groups are +`service/resourceusage` (9, the resource-usage ledger), `service/uploads/blossom` +(`InFlightInvoices`, `PaymentPromptLedger`, the `bud10` resolver trio), +`service/playback` leaves (`HlsLivenessCache`, `SessionRegistry`, +`WssDataStreamCollector`, the player control buttons), and `model/MediaAspectRatioCache`. diff --git a/commons/plans/README.md b/commons/plans/README.md index 01bc81b0cf..faf2fdd620 100644 --- a/commons/plans/README.md +++ b/commons/plans/README.md @@ -1,18 +1,19 @@ # commons plans -_Audited 2026-06-30 (+ 2026-09-12 split entry). 7 plans: 3 shipped, 2 in-progress, 2 queued, 0 abandoned._ +_Audited 2026-06-30 (+ 2026-09-12 split entry, 2026-09-27 migration entries)._ ## In progress | Plan | Summary | | ---- | ------- | | [2026-05-04-custom-feeds-plan.md](2026-05-04-custom-feeds-plan.md) | Custom feed creation/discovery/management for Desktop; core model + builder + kind 31890 + desktop UI shipped, but relay-filter layer, DVM marketplace, kind 10090 sync, and list resolution still pending. | | [2026-05-06-nest-subscription-manager-extraction.md](2026-05-06-nest-subscription-manager-extraction.md) | Split the per-speaker subscription state machine out of `NestViewModel`; only the `ActiveSubscription` stepping-stone is extracted so far. | +| [2026-08-30-commons-migration-sweep.md](2026-08-30-commons-migration-sweep.md) | The running tracker for moving `amethyst/` code into `commons`/`commonsUI`: waves, what moved each round, what stays and why. `LocalCache` has moved; retiring `DesktopLocalCache` (Wave 2 part B) and the `AccountViewModel` hub composables are next. | +| [2026-05-30-amethyst-to-commons-migration.md](2026-05-30-amethyst-to-commons-migration.md) | The original roadmap for the same move; superseded in practice by the sweep tracker above. | ## Queued | Plan | Summary | | ---- | ------- | | [2026-04-21-event-renderer.md](2026-04-21-event-renderer.md) | Cross-platform UI-agnostic `RenderedEvent` subsystem shared by Amy, Desktop, and Android; not started. | -| [2026-05-30-amethyst-to-commons-migration.md](2026-05-30-amethyst-to-commons-migration.md) | Roadmap to move shared `amethyst` Android code into `commons`; keystone `Account`/`LocalCache` extraction not begun. | | [2026-08-03-poll-results-page.md](2026-08-03-poll-results-page.md) | Extended NIP-88 poll results page (per-option counts + who voted for what) for Android and Desktop; also specifies four tally-correctness fixes and the missing poll-relay subscription. Proposed, not started. | ## Shipped diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/PitchShifter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/PitchShifter.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/PitchShifter.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/PitchShifter.kt index cb620f8294..d022792b57 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/PitchShifter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/PitchShifter.kt @@ -18,9 +18,8 @@ * 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.actions.uploads +package com.vitorpamplona.amethyst.commons.audio -import com.vitorpamplona.amethyst.commons.audio.AudioWindow import kotlin.math.roundToInt /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/FilterByListParams.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/FilterByListParams.kt index 9715816619..71ea95206f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/FilterByListParams.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.dal +package com.vitorpamplona.amethyst.commons.feeds import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/StringFeedState.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedState.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/StringFeedState.kt index f63f6830af..011df2b7d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/StringFeedState.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings +package com.vitorpamplona.amethyst.commons.feeds import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.MutableStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/SupportedContent.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/SupportedContent.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/SupportedContent.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/SupportedContent.kt index fee279e1f1..fc9fe90180 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/SupportedContent.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/SupportedContent.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal +package com.vitorpamplona.amethyst.commons.feeds import com.vitorpamplona.amethyst.commons.richtext.RichTextParser diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/UserFeedState.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedState.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/UserFeedState.kt index 1659f47cef..4cc72b76eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/feeds/UserFeedState.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen +package com.vitorpamplona.amethyst.commons.feeds import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.model.User diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordLastRead.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ConcordLastRead.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordLastRead.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ConcordLastRead.kt index 65b819fd52..3fffed0ce4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/concord/ConcordLastRead.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ConcordLastRead.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.concord +package com.vitorpamplona.amethyst.commons.model /** * The per-account last-read route key for one Concord channel — the string diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupLastRead.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/MarmotGroupLastRead.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupLastRead.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/MarmotGroupLastRead.kt index 252fb79e8a..b5fc79c21f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/marmotGroup/MarmotGroupLastRead.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/MarmotGroupLastRead.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup +package com.vitorpamplona.amethyst.commons.model import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupLastRead.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/RelayGroupLastRead.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupLastRead.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/RelayGroupLastRead.kt index f5f563f050..639bd5017d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/RelayGroupLastRead.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/RelayGroupLastRead.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup +package com.vitorpamplona.amethyst.commons.model import com.vitorpamplona.quartz.nip29RelayGroups.GroupId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWork.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/buzz/AgentWork.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWork.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/buzz/AgentWork.kt index fa7b3412a7..98d2de1c56 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/AgentWork.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/buzz/AgentWork.kt @@ -18,13 +18,9 @@ * 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.buzz +package com.vitorpamplona.amethyst.commons.model.buzz import androidx.compose.runtime.Immutable -import com.vitorpamplona.amethyst.commons.model.buzz.JobState -import com.vitorpamplona.amethyst.commons.model.buzz.JobView -import com.vitorpamplona.amethyst.commons.model.buzz.WorkflowRun -import com.vitorpamplona.amethyst.commons.model.buzz.WorkflowRunState import com.vitorpamplona.quartz.nip01Core.core.HexKey /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ConcordServerRoomNote.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/chats/ConcordServerRoomNote.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ConcordServerRoomNote.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/chats/ConcordServerRoomNote.kt index e591edb75a..7e3d987f7f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ConcordServerRoomNote.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/chats/ConcordServerRoomNote.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal +package com.vitorpamplona.amethyst.commons.model.chats import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/RelayGroupServerRoomNote.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/chats/RelayGroupServerRoomNote.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/RelayGroupServerRoomNote.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/chats/RelayGroupServerRoomNote.kt index 4aab0fb043..bb5b4dc8d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/RelayGroupServerRoomNote.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/chats/RelayGroupServerRoomNote.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal +package com.vitorpamplona.amethyst.commons.model.chats import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/draftTags/DraftTagState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/DraftTagState.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/draftTags/DraftTagState.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/DraftTagState.kt index a014a1627e..404291aa9f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/draftTags/DraftTagState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/DraftTagState.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.note.creators.draftTags +package com.vitorpamplona.amethyst.commons.model.composer import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/IExpiration.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/IExpiration.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/IExpiration.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/IExpiration.kt index 6d3a77fdbf..10c359151c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/IExpiration.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/IExpiration.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.note.creators.expiration +package com.vitorpamplona.amethyst.commons.model.composer import androidx.compose.runtime.Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/NewMessageTagger.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/NewMessageTagger.kt index 3d2e20ad2e..368c2f6d1f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/NewMessageTagger.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.actions +package com.vitorpamplona.amethyst.commons.model.composer import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.commons.model.Dao diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/SplitConversor.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/SplitConversor.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/SplitConversor.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/SplitConversor.kt index c492243bba..e51b41c4de 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/SplitConversor.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/composer/SplitConversor.kt @@ -18,10 +18,9 @@ * 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.creators.zapsplits +package com.vitorpamplona.amethyst.commons.model.composer import com.vitorpamplona.amethyst.commons.model.User -import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import kotlin.math.round diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteTextArgs.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/RouteTextArgs.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteTextArgs.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/RouteTextArgs.kt index 0c54d0e01b..ced966ada8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteTextArgs.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/RouteTextArgs.kt @@ -18,7 +18,10 @@ * 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.navigation.routes +package com.vitorpamplona.amethyst.commons.model.navigation + +import com.vitorpamplona.amethyst.commons.util.codePointAtKmp +import com.vitorpamplona.amethyst.commons.util.codePointCharCount /** * Navigation routes travel as a URI string: `…Route.Room/{id}?message={message}&…`, with every @@ -77,14 +80,14 @@ fun String.limitToRouteTextArg(maxEncodedLength: Int = MAX_ROUTE_TEXT_ARG_ENCODE var cutAt = 0 while (index < length) { // Steps over whole code points so a surrogate pair is never cut in half. - val codePoint = codePointAt(index) + val codePoint = codePointAtKmp(index) spent += encodedCodePointLength(codePoint) if (spent > maxEncodedLength) { // Only here is truncation certain. [cutAt] is the last position whose prefix still // leaves room for the marker; a budget too small to hold even that yields nothing. return if (budget >= 0) substring(0, cutAt) + ROUTE_TEXT_ARG_TRUNCATION_MARKER else "" } - index += Character.charCount(codePoint) + index += codePointCharCount(codePoint) if (spent <= budget) cutAt = index } return this @@ -95,9 +98,9 @@ fun encodedRouteArgLength(text: String): Int { var total = 0 var index = 0 while (index < text.length) { - val codePoint = text.codePointAt(index) + val codePoint = text.codePointAtKmp(index) total += encodedCodePointLength(codePoint) - index += Character.charCount(codePoint) + index += codePointCharCount(codePoint) } return total } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMRouteRewriter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/ShareToDMRouteRewriter.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMRouteRewriter.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/ShareToDMRouteRewriter.kt index a17a3b5805..d2d49c61a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMRouteRewriter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/ShareToDMRouteRewriter.kt @@ -18,9 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.share - -import com.vitorpamplona.amethyst.commons.model.navigation.Route +package com.vitorpamplona.amethyst.commons.model.navigation /** * Injects shared content into a chatroom navigation so that tapping a recent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/dal/GroupDiscoveryConstraint.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/GroupDiscoveryConstraintResolver.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/dal/GroupDiscoveryConstraint.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/GroupDiscoveryConstraintResolver.kt index 69f7969579..6ba7b1c6b2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/dal/GroupDiscoveryConstraint.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/GroupDiscoveryConstraintResolver.kt @@ -18,9 +18,8 @@ * 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.chats.publicChannels.relayGroup.dal +package com.vitorpamplona.amethyst.commons.model.nip29RelayGroups -import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.GroupDiscoveryConstraint import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/TransactionRowLabels.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip47WalletConnect/TransactionRowLabels.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/TransactionRowLabels.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip47WalletConnect/TransactionRowLabels.kt index 6f157fd4c8..1bbeac7d5b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/TransactionRowLabels.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip47WalletConnect/TransactionRowLabels.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet +package com.vitorpamplona.amethyst.commons.model.nip47WalletConnect import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransaction import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransactionType diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityRulesLookup.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip72Communities/CommunityRulesLookup.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityRulesLookup.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip72Communities/CommunityRulesLookup.kt index 5393ac47f4..ce3cdcc2ff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityRulesLookup.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip72Communities/CommunityRulesLookup.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal +package com.vitorpamplona.amethyst.commons.model.nip72Communities import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.cache.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ImetaContent.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip92IMeta/ImetaContent.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ImetaContent.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip92IMeta/ImetaContent.kt index 8ddd7facc2..7e886ec19d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ImetaContent.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip92IMeta/ImetaContent.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.note.types +package com.vitorpamplona.amethyst.commons.model.nip92IMeta import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip92IMeta.imetas diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicFormatting.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/music/MusicFormatting.kt similarity index 89% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicFormatting.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/music/MusicFormatting.kt index 70c9814545..3a03cc1f9c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MusicFormatting.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/music/MusicFormatting.kt @@ -18,11 +18,11 @@ * 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 +package com.vitorpamplona.amethyst.commons.music /** `m:ss` formatting for a track length in seconds, shared by both MusicTrack and MusicPlaylist UI. */ -internal fun formatTrackDuration(seconds: Int): String { +fun formatTrackDuration(seconds: Int): String { val minutes = seconds / 60 val secs = seconds % 60 - return "%d:%02d".format(minutes, secs) + return "$minutes:${secs.toString().padStart(2, '0')}" } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/ScanResult.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/qrcode/ScanResult.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/ScanResult.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/qrcode/ScanResult.kt index ab0c14b126..383262c3ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/ScanResult.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/qrcode/ScanResult.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.scanner +package com.vitorpamplona.amethyst.commons.qrcode /** A point in the decoder's image space (origin top-left, after crop and rotation). */ data class ScanPoint( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/StructuredAppendAccumulator.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/qrcode/StructuredAppendAccumulator.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/StructuredAppendAccumulator.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/qrcode/StructuredAppendAccumulator.kt index 26a0c8cc77..94b5112521 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/StructuredAppendAccumulator.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/qrcode/StructuredAppendAccumulator.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.scanner +package com.vitorpamplona.amethyst.commons.qrcode /** * Reassembles a Structured Append payload — one logical string split across several physical QR diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsFilterAssembler.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup/RelayGroupOpenThreadsFilterAssembler.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsFilterAssembler.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup/RelayGroupOpenThreadsFilterAssembler.kt index 72de0c6434..4345dd6eca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupOpenThreadsFilterAssembler.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup/RelayGroupOpenThreadsFilterAssembler.kt @@ -18,10 +18,9 @@ * 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.chats.publicChannels.relayGroup.datasource +package com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel -import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.buildRelayGroupThreadsFilter import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsDiscoveryFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup/RelayGroupsDiscoveryFilter.kt similarity index 78% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsDiscoveryFilter.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup/RelayGroupsDiscoveryFilter.kt index 96a00d8091..ef81e16e47 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/datasource/RelayGroupsDiscoveryFilter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/channel/relayGroup/RelayGroupsDiscoveryFilter.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.datasource +package com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.amethyst.commons.model.topNavFeeds.IFeedTopNavPerRelayFilterSet @@ -31,15 +31,6 @@ import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.author.Aut import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.relay.RelayTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsByAllCommunities -import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsByAuthors -import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsByCommunity -import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsByFollows -import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsByGeohashes -import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsByHashtag -import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsByMutedAuthors -import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsByRelay -import com.vitorpamplona.amethyst.commons.relayClient.channel.relayGroup.filterRelayGroupsGlobal import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.scopedTo import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingEphemeralChats.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatrooms/FilterFollowingEphemeralChats.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingEphemeralChats.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatrooms/FilterFollowingEphemeralChats.kt index 9279e7b853..e1b3655e1b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingEphemeralChats.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatrooms/FilterFollowingEphemeralChats.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource +package com.vitorpamplona.amethyst.commons.relayClient.chatrooms import com.vitorpamplona.amethyst.commons.defaults.Constants import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingPublicChats.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatrooms/FilterFollowingPublicChats.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingPublicChats.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatrooms/FilterFollowingPublicChats.kt index 07ff49216d..3dcb6fc825 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingPublicChats.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatrooms/FilterFollowingPublicChats.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource +package com.vitorpamplona.amethyst.commons.relayClient.chatrooms import com.vitorpamplona.amethyst.commons.defaults.Constants import com.vitorpamplona.amethyst.commons.model.cache.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterLastMessageFollowingPublicChats.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatrooms/FilterLastMessageFollowingPublicChats.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterLastMessageFollowingPublicChats.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatrooms/FilterLastMessageFollowingPublicChats.kt index 5d17ed04d6..a96c62d3f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterLastMessageFollowingPublicChats.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatrooms/FilterLastMessageFollowingPublicChats.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource +package com.vitorpamplona.amethyst.commons.relayClient.chatrooms import com.vitorpamplona.amethyst.commons.defaults.Constants import com.vitorpamplona.amethyst.commons.model.cache.LocalCache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatrooms/FilterNip04DMsFromMe.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatrooms/FilterNip04DMsFromMe.kt index 516c98b011..898bf515f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatrooms/FilterNip04DMsFromMe.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource +package com.vitorpamplona.amethyst.commons.relayClient.chatrooms import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatrooms/FilterNip04DMsToMe.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatrooms/FilterNip04DMsToMe.kt index d7b68ba331..ab1c19e77d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/chatrooms/FilterNip04DMsToMe.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource +package com.vitorpamplona.amethyst.commons.relayClient.chatrooms import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocacheListingKinds.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/geocaches/GeocacheListingKinds.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocacheListingKinds.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/geocaches/GeocacheListingKinds.kt index 0444eb635e..9be06768db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/dal/GeocacheListingKinds.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/geocaches/GeocacheListingKinds.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.dal +package com.vitorpamplona.amethyst.commons.relayClient.geocaches import com.vitorpamplona.quartz.nipCCGeocaching.listing.GeocacheListingEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/hashtag/FilterPostsByHashtags.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/hashtag/FilterPostsByHashtags.kt index 2586985599..4a47e35c1e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/hashtag/FilterPostsByHashtags.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource +package com.vitorpamplona.amethyst.commons.relayClient.hashtag import com.vitorpamplona.amethyst.commons.relayClient.nip22Comments.CommentKinds import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.ExplainedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/results/RelayPollResponseLoader.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/polls/results/RelayPollResponseLoader.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/results/RelayPollResponseLoader.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/polls/results/RelayPollResponseLoader.kt index b1bf7a931e..c3ebf26beb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/polls/results/RelayPollResponseLoader.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/polls/results/RelayPollResponseLoader.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.results +package com.vitorpamplona.amethyst.commons.relayClient.polls.results import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.viewmodels.nip88Polls.PollLoadReport diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/HlsPublishState.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishState.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/HlsPublishState.kt index 6513aeb45f..6f5669bce3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/HlsPublishState.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.hls +package com.vitorpamplona.amethyst.commons.service.upload sealed class HlsPublishState { data object Idle : HlsPublishState() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/MediaUploadTracker.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/MediaUploadTracker.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/MediaUploadTracker.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/MediaUploadTracker.kt index 6ff5e72ec8..4d344e8514 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/MediaUploadTracker.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/MediaUploadTracker.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.actions.uploads +package com.vitorpamplona.amethyst.commons.service.upload import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/SuspendableConfirmation.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/SuspendableConfirmation.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/SuspendableConfirmation.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/SuspendableConfirmation.kt index ca94a7728e..44a457a83c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/SuspendableConfirmation.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/SuspendableConfirmation.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.service.uploads +package com.vitorpamplona.amethyst.commons.service.upload import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorDialogViewModel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/tor/TorDialogViewModel.kt similarity index 90% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorDialogViewModel.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/tor/TorDialogViewModel.kt index ffae7081c7..8158665f84 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorDialogViewModel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/tor/TorDialogViewModel.kt @@ -18,20 +18,12 @@ * 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.tor +package com.vitorpamplona.amethyst.commons.tor import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel -import com.vitorpamplona.amethyst.commons.tor.TorPresetType -import com.vitorpamplona.amethyst.commons.tor.TorSettings -import com.vitorpamplona.amethyst.commons.tor.TorType -import com.vitorpamplona.amethyst.commons.tor.torDefaultPreset -import com.vitorpamplona.amethyst.commons.tor.torFullyPrivate -import com.vitorpamplona.amethyst.commons.tor.torOnlyWhenNeededPreset -import com.vitorpamplona.amethyst.commons.tor.torSmallPayloadsPreset -import com.vitorpamplona.amethyst.commons.tor.whichPreset @Stable class TorDialogViewModel : ViewModel() { @@ -92,7 +84,7 @@ class TorDialogViewModel : ViewModel() { fun save(): TorSettings = TorSettings( torType = torType.value, - externalSocksPort = Integer.parseInt(socksPortStr.value), + externalSocksPort = socksPortStr.value.toInt(), onionRelaysViaTor = onionRelaysViaTor.value, dmRelaysViaTor = dmRelaysViaTor.value, newRelaysViaTor = newRelaysViaTor.value, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DebouncedPublisher.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/DebouncedPublisher.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DebouncedPublisher.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/DebouncedPublisher.kt index 71e72118c0..eff052fcb0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DebouncedPublisher.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/DebouncedPublisher.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn +package com.vitorpamplona.amethyst.commons.util import kotlinx.coroutines.Job import kotlinx.coroutines.delay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/RelayFeedViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/RelayFeedViewModel.kt index 3c10216804..be771d2bf8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/RelayFeedViewModel.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays +package com.vitorpamplona.amethyst.commons.viewmodels import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable @@ -36,6 +36,7 @@ import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.IO import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -195,6 +196,6 @@ class RelayFeedViewModel : } override fun onCleared() { - Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } + Log.d("Init") { "OnCleared: ${this::class.simpleName}" } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/identity/UserExternalIdentitiesViewModel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/UserExternalIdentitiesViewModel.kt similarity index 88% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/identity/UserExternalIdentitiesViewModel.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/UserExternalIdentitiesViewModel.kt index 9a5c175da6..217b87ec7d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/identity/UserExternalIdentitiesViewModel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/UserExternalIdentitiesViewModel.kt @@ -18,21 +18,24 @@ * 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.profile.header.identity +package com.vitorpamplona.amethyst.commons.viewmodels import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.CreationExtras import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent import com.vitorpamplona.quartz.nip39ExtIdentities.identityClaims import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlin.reflect.KClass @Stable class UserExternalIdentitiesViewModel( @@ -61,6 +64,9 @@ class UserExternalIdentitiesViewModel( val user: User, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun <T : ViewModel> create(modelClass: Class<T>): T = UserExternalIdentitiesViewModel(user) as T + override fun <T : ViewModel> create( + modelClass: KClass<T>, + extras: CreationExtras, + ): T = UserExternalIdentitiesViewModel(user) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/wizard/CashuWalletDiscovery.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/CashuWalletDiscovery.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/wizard/CashuWalletDiscovery.kt rename to commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/CashuWalletDiscovery.kt index 41bd6b695a..54aae743e5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/wizard/CashuWalletDiscovery.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/CashuWalletDiscovery.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.wizard +package com.vitorpamplona.amethyst.commons.cashu import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServerHealth.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/mediaServers/MediaServerHealth.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServerHealth.kt rename to commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/mediaServers/MediaServerHealth.kt index 23e8c85326..7e338ac74c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/MediaServerHealth.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/mediaServers/MediaServerHealth.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.actions.mediaServers +package com.vitorpamplona.amethyst.commons.mediaServers import com.vitorpamplona.quartz.nipB7Blossom.BlossomServerUrl import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/HistoryDateFormat.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/chats/HistoryDateFormat.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/HistoryDateFormat.kt rename to commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/chats/HistoryDateFormat.kt index 7b6ab94391..cf3fd25fb0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/HistoryDateFormat.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/chats/HistoryDateFormat.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed +package com.vitorpamplona.amethyst.commons.model.chats import java.text.SimpleDateFormat import java.util.Date diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewState.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/composer/PreviewState.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewState.kt rename to commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/composer/PreviewState.kt index ad97ee17c2..b5c748ecd5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewState.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/model/composer/PreviewState.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.note.creators.previews +package com.vitorpamplona.amethyst.commons.model.composer import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.richtext.CachedUrlParser diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/TimeFormatUtils.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeFormatUtils.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/TimeFormatUtils.kt rename to commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeFormatUtils.kt index 3ff304ae67..ce09af14e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/TimeFormatUtils.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeFormatUtils.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.actions.uploads +package com.vitorpamplona.amethyst.commons.util import java.util.Locale diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/ZapFormatter.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/ZapFormatter.kt index 6879dc34d5..a0ad25666c 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/ZapFormatter.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/ZapFormatter.kt @@ -24,12 +24,12 @@ import java.math.BigDecimal import java.math.RoundingMode import java.text.DecimalFormat -private val TenGiga = BigDecimal(10_000_000_000) -private val OneGiga = BigDecimal(1_000_000_000) -private val TenMega = BigDecimal(10_000_000) -private val OneMega = BigDecimal(1_000_000) -private val TenKilo = BigDecimal(10_000) -private val OneKilo = BigDecimal(1_000) +val TenGiga = BigDecimal(10_000_000_000) +val OneGiga = BigDecimal(1_000_000_000) +val TenMega = BigDecimal(10_000_000) +val OneMega = BigDecimal(1_000_000) +val TenKilo = BigDecimal(10_000) +val OneKilo = BigDecimal(1_000) private val dfGBig = ThreadLocal.withInitial { DecimalFormat("#.#G") } private val dfGSmall = ThreadLocal.withInitial { DecimalFormat("#.0G") } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatterNoDecimals.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/ZapFormatterNoDecimals.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatterNoDecimals.kt rename to commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/ZapFormatterNoDecimals.kt index ab8aa20159..5207240aef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatterNoDecimals.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/ZapFormatterNoDecimals.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.note +package com.vitorpamplona.amethyst.commons.util import java.math.BigDecimal import java.math.RoundingMode diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/actions/uploads/PitchShifterTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/audio/PitchShifterTest.kt similarity index 98% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/ui/actions/uploads/PitchShifterTest.kt rename to commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/audio/PitchShifterTest.kt index 318f7690b5..de3b04e36e 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/actions/uploads/PitchShifterTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/audio/PitchShifterTest.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.actions.uploads +package com.vitorpamplona.amethyst.commons.audio import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/SupportedContentTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/feeds/SupportedContentTest.kt similarity index 98% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/SupportedContentTest.kt rename to commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/feeds/SupportedContentTest.kt index e6e0029e17..8cffb4db36 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/SupportedContentTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/feeds/SupportedContentTest.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal +package com.vitorpamplona.amethyst.commons.feeds import junit.framework.TestCase.assertTrue import org.junit.Test diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/composer/NewMessageTaggerKeyParseTest.kt similarity index 98% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt rename to commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/composer/NewMessageTaggerKeyParseTest.kt index f126658623..3cc2675f47 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/composer/NewMessageTaggerKeyParseTest.kt @@ -18,14 +18,13 @@ * 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 +package com.vitorpamplona.amethyst.commons.model.composer import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Dao import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.cache.LocalCache.getOrCreateAddressableNoteInternal -import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NPub diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/RouteTextArgsTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/RouteTextArgsTest.kt similarity index 91% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/RouteTextArgsTest.kt rename to commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/RouteTextArgsTest.kt index 00fe91a596..74f33677d2 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/RouteTextArgsTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/RouteTextArgsTest.kt @@ -18,12 +18,8 @@ * 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.navigation +package com.vitorpamplona.amethyst.commons.model.navigation -import com.vitorpamplona.amethyst.ui.navigation.routes.MAX_ROUTE_TEXT_ARG_ENCODED_LENGTH -import com.vitorpamplona.amethyst.ui.navigation.routes.ROUTE_TEXT_ARG_TRUNCATION_MARKER -import com.vitorpamplona.amethyst.ui.navigation.routes.encodedRouteArgLength -import com.vitorpamplona.amethyst.ui.navigation.routes.limitToRouteTextArg import org.junit.Assert.assertEquals import org.junit.Assert.assertSame import org.junit.Assert.assertTrue diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/ShareToDMRouteRewriterTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/ShareToDMRouteRewriterTest.kt similarity index 91% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/ShareToDMRouteRewriterTest.kt rename to commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/ShareToDMRouteRewriterTest.kt index 1758af9418..8765dca0f9 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/navigation/ShareToDMRouteRewriterTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/navigation/ShareToDMRouteRewriterTest.kt @@ -18,10 +18,8 @@ * 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.navigation +package com.vitorpamplona.amethyst.commons.model.navigation -import com.vitorpamplona.amethyst.commons.model.navigation.Route -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.share.ShareToDMRouteRewriter import org.junit.Assert.assertEquals import org.junit.Assert.assertSame import org.junit.Test diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/dal/RelayGroupDiscoveryConstraintTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/RelayGroupDiscoveryConstraintTest.kt similarity index 96% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/dal/RelayGroupDiscoveryConstraintTest.kt rename to commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/RelayGroupDiscoveryConstraintTest.kt index efaf124679..45275e84bc 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/relayGroup/dal/RelayGroupDiscoveryConstraintTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip29RelayGroups/RelayGroupDiscoveryConstraintTest.kt @@ -18,10 +18,8 @@ * 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.chats.publicChannels.relayGroup.dal +package com.vitorpamplona.amethyst.commons.model.nip29RelayGroups -import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.GroupDiscoveryConstraint -import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel import com.vitorpamplona.amethyst.commons.model.topNavFeeds.global.GlobalTopNavPerRelayFilter import com.vitorpamplona.amethyst.commons.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.commons.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilter diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/wallet/TransactionRowLabelsTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip47WalletConnect/TransactionRowLabelsTest.kt similarity index 97% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/ui/wallet/TransactionRowLabelsTest.kt rename to commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip47WalletConnect/TransactionRowLabelsTest.kt index 6e34dfe90e..fd378e4035 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/wallet/TransactionRowLabelsTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip47WalletConnect/TransactionRowLabelsTest.kt @@ -18,9 +18,8 @@ * 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.wallet +package com.vitorpamplona.amethyst.commons.model.nip47WalletConnect -import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.TransactionRowLabels import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransaction import org.junit.Assert.assertEquals import org.junit.Assert.assertNull diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityRulesLookupTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip72Communities/CommunityRulesLookupTest.kt similarity index 98% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityRulesLookupTest.kt rename to commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip72Communities/CommunityRulesLookupTest.kt index 8d73566da6..8d6a3bf7b2 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/dal/CommunityRulesLookupTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/model/nip72Communities/CommunityRulesLookupTest.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal +package com.vitorpamplona.amethyst.commons.model.nip72Communities import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/StructuredAppendAccumulatorTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/qrcode/StructuredAppendAccumulatorTest.kt similarity index 98% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/StructuredAppendAccumulatorTest.kt rename to commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/qrcode/StructuredAppendAccumulatorTest.kt index e20b9106b4..c679704a4e 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/StructuredAppendAccumulatorTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/qrcode/StructuredAppendAccumulatorTest.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.scanner +package com.vitorpamplona.amethyst.commons.qrcode import org.junit.Assert.assertEquals import org.junit.Assert.assertNull diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DebouncedPublisherTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/util/DebouncedPublisherTest.kt similarity index 98% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DebouncedPublisherTest.kt rename to commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/util/DebouncedPublisherTest.kt index b7b26673cf..d90d6a823a 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DebouncedPublisherTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/util/DebouncedPublisherTest.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn +package com.vitorpamplona.amethyst.commons.util import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/account/ui/login/LoginErrorManager.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/account/ui/login/LoginErrorManager.kt index b07884c2ac..50a91b33bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/account/ui/login/LoginErrorManager.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedOff.login +package com.vitorpamplona.amethyst.commons.account.ui.login import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/VoicePreset.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/VoicePreset.kt index 9b87c11b77..1ac2fb77ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/VoicePreset.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.actions.uploads +package com.vitorpamplona.amethyst.commons.audio import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.voice_preset_deep diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/ClinkBudgetDialog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/clink/ui/ClinkBudgetDialog.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/ClinkBudgetDialog.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/clink/ui/ClinkBudgetDialog.kt index 020b4ad7c0..59ddc9f118 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/ClinkBudgetDialog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/clink/ui/ClinkBudgetDialog.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet +package com.vitorpamplona.amethyst.commons.clink.ui import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/BusyLabel.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/cordn/ui/BusyLabel.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/BusyLabel.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/cordn/ui/BusyLabel.kt index c916830d4c..ae3c0b5c99 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/BusyLabel.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/cordn/ui/BusyLabel.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn +package com.vitorpamplona.amethyst.commons.cordn.ui import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/SettingsFormBlock.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/cordn/ui/SettingsFormBlock.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/SettingsFormBlock.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/cordn/ui/SettingsFormBlock.kt index ca8825fec0..e66b6b51e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/SettingsFormBlock.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/cordn/ui/SettingsFormBlock.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.cordn +package com.vitorpamplona.amethyst.commons.cordn.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46ActivityUi.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip46RemoteSigner/ui/Nip46ActivityUi.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46ActivityUi.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip46RemoteSigner/ui/Nip46ActivityUi.kt index e4180fef22..5ed65fb057 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46ActivityUi.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip46RemoteSigner/ui/Nip46ActivityUi.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46 +package com.vitorpamplona.amethyst.commons.nip46RemoteSigner.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsViewMode.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip52Calendar/ui/CalendarsViewMode.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsViewMode.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip52Calendar/ui/CalendarsViewMode.kt index 9dd2439014..ac135fc16d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarsViewMode.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip52Calendar/ui/CalendarsViewMode.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars +package com.vitorpamplona.amethyst.commons.nip52Calendar.ui import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.calendar_view_day diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommunityRulesViolationBanner.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip72ModCommunities/ui/CommunityRulesViolationBanner.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommunityRulesViolationBanner.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip72ModCommunities/ui/CommunityRulesViolationBanner.kt index 064a3e2748..48f278e49d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommunityRulesViolationBanner.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip72ModCommunities/ui/CommunityRulesViolationBanner.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.note.nip22Comments +package com.vitorpamplona.amethyst.commons.nip72ModCommunities.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.Row diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/SubPurposeLabels.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relays/ui/SubPurposeLabels.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/SubPurposeLabels.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relays/ui/SubPurposeLabels.kt index 8cde025250..50717b3f36 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/SubPurposeLabels.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relays/ui/SubPurposeLabels.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common +package com.vitorpamplona.amethyst.commons.relays.ui import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurpose import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.SubPurposeGroup diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/StrippingFailureDialog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/ui/StrippingFailureDialog.kt similarity index 91% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/StrippingFailureDialog.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/ui/StrippingFailureDialog.kt index 2649999976..2f528636bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/StrippingFailureDialog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/ui/StrippingFailureDialog.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.actions +package com.vitorpamplona.amethyst.commons.service.upload.ui import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button @@ -29,9 +29,9 @@ import com.vitorpamplona.amethyst.commons.resources.cancel import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_body import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_title import com.vitorpamplona.amethyst.commons.resources.metadata_strip_failed_upload +import com.vitorpamplona.amethyst.commons.service.upload.ConfirmationCallbacks +import com.vitorpamplona.amethyst.commons.service.upload.SuspendableConfirmation import com.vitorpamplona.amethyst.commons.ui.stringRes -import com.vitorpamplona.amethyst.service.uploads.ConfirmationCallbacks -import com.vitorpamplona.amethyst.service.uploads.SuspendableConfirmation @Composable fun StrippingFailureDialog(confirmation: SuspendableConfirmation) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/KeyboardState.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/insets/KeyboardState.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/KeyboardState.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/insets/KeyboardState.kt index 4d9a7fde2d..109b8e4260 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/KeyboardState.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/insets/KeyboardState.kt @@ -18,14 +18,13 @@ * 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.navigation.bottombars +package com.vitorpamplona.amethyst.commons.ui.insets import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalDensity -import com.vitorpamplona.amethyst.commons.ui.insets.rememberSafeImeInsets enum class KeyboardState { Opened, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/TabReselectCoordinator.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/bottombars/TabReselectCoordinator.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/TabReselectCoordinator.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/bottombars/TabReselectCoordinator.kt index 936bbc16fc..dee5d92392 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/TabReselectCoordinator.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/bottombars/TabReselectCoordinator.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.navigation.bottombars +package com.vitorpamplona.amethyst.commons.ui.navigation.bottombars import androidx.compose.runtime.Stable import androidx.compose.runtime.staticCompositionLocalOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/ObservableNav.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/navs/ObservableNav.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/ObservableNav.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/navs/ObservableNav.kt index a21f8fd611..63b60e2bb9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/ObservableNav.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/navs/ObservableNav.kt @@ -18,10 +18,9 @@ * 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.navigation.navs +package com.vitorpamplona.amethyst.commons.ui.navigation.navs import com.vitorpamplona.amethyst.commons.model.navigation.Route -import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlin.reflect.KClass diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMNav.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/navs/ShareToDMNav.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMNav.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/navs/ShareToDMNav.kt index 0ac8cadba6..daf4eb5422 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/share/ShareToDMNav.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/navs/ShareToDMNav.kt @@ -18,11 +18,11 @@ * 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.chats.share +package com.vitorpamplona.amethyst.commons.ui.navigation.navs import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.model.navigation.Route -import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.model.navigation.ShareToDMRouteRewriter /** * Wraps an [INav] so that navigating to a chatroom ([Route.Room]) from the diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/navs/TwoPaneNav.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/navs/TwoPaneNav.kt index fbc5eeae1c..add3728387 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/navigation/navs/TwoPaneNav.kt @@ -18,11 +18,10 @@ * 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.chats.rooms.twopane +package com.vitorpamplona.amethyst.commons.ui.navigation.navs import androidx.compose.runtime.mutableStateOf import com.vitorpamplona.amethyst.commons.model.navigation.Route -import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalog.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/settings/SettingsCatalog.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalog.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/settings/SettingsCatalog.kt index 41a2de97c9..5260295216 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalog.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/settings/SettingsCatalog.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings +package com.vitorpamplona.amethyst.commons.ui.settings import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import org.jetbrains.compose.resources.StringResource diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MentionPreservingInputTransformation.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/text/MentionPreservingInputTransformation.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MentionPreservingInputTransformation.kt rename to commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/text/MentionPreservingInputTransformation.kt index 608099835a..3a35dd38ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MentionPreservingInputTransformation.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/text/MentionPreservingInputTransformation.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.actions +package com.vitorpamplona.amethyst.commons.ui.text import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.text.input.InputTransformation @@ -26,11 +26,11 @@ import androidx.compose.foundation.text.input.TextFieldBuffer /** * Matches a complete Nostr mention token: `@npub1…`, `nostr:npub1…`, - * `@nprofile1…`, `nostr:nprofile1…`. Shared with [UrlUserTagOutputTransformation] + * `@nprofile1…`, `nostr:nprofile1…`. Shared with the app's `UrlUserTagOutputTransformation` * so the wedge it produces and the input-side guard below agree on what * counts as a mention. */ -internal val MENTION_REGEX = Regex("(?:@|nostr:)(?:npub1[a-z0-9]{58}|nprofile1[a-z0-9]+)") +val MENTION_REGEX = Regex("(?:@|nostr:)(?:npub1[a-z0-9]{58}|nprofile1[a-z0-9]+)") /** * Keeps Nostr mentions atomic against IME edits that would only modify part of diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordingIndicators.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/audio/RecordingIndicators.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordingIndicators.kt rename to commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/audio/RecordingIndicators.kt index 2926375967..be2eb5eced 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordingIndicators.kt +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/audio/RecordingIndicators.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.actions.uploads +package com.vitorpamplona.amethyst.commons.audio import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode @@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.recording_indicator_description import com.vitorpamplona.amethyst.commons.resources.recording_indicator_with_time import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.util.formatSecondsToTime /** * Animated expanding circles that pulse outward from the recording button diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/StreamingStatusFlags.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip53LiveActivities/ui/StreamingStatusFlags.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/StreamingStatusFlags.kt rename to commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip53LiveActivities/ui/StreamingStatusFlags.kt index 802856fe13..7528de89ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/StreamingStatusFlags.kt +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip53LiveActivities/ui/StreamingStatusFlags.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities +package com.vitorpamplona.amethyst.commons.nip53LiveActivities.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.padding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/InformationDialog.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/components/InformationDialog.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/InformationDialog.kt rename to commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/components/InformationDialog.kt index 32e9e00590..1c7509449e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/InformationDialog.kt +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/components/InformationDialog.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.actions +package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewCard.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt rename to commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewCard.kt index d3e60a8a4d..95ff1c5d00 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/UrlPreviewCard.kt +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/components/UrlPreviewCard.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.components +package com.vitorpamplona.amethyst.commons.ui.components import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable @@ -48,9 +48,6 @@ import com.vitorpamplona.amethyst.commons.resources.copy_url_to_clipboard import com.vitorpamplona.amethyst.commons.resources.kind_comments import com.vitorpamplona.amethyst.commons.resources.link_actions_dialog_title import com.vitorpamplona.amethyst.commons.resources.url_preview_open_in_browser -import com.vitorpamplona.amethyst.commons.ui.components.M3ActionDialog -import com.vitorpamplona.amethyst.commons.ui.components.M3ActionRow -import com.vitorpamplona.amethyst.commons.ui.components.M3ActionSection import com.vitorpamplona.amethyst.commons.ui.components.util.setText import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/ColorSchemePreview.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/ColorSchemePreview.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/ColorSchemePreview.kt rename to commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/ColorSchemePreview.kt index a0bff9549d..f964c23093 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/ColorSchemePreview.kt +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/theme/ColorSchemePreview.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.theme +package com.vitorpamplona.amethyst.commons.ui.theme import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -48,7 +48,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow import kotlin.math.max import kotlin.math.min diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogFilterTest.kt b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/settings/SettingsCatalogFilterTest.kt similarity index 98% rename from amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogFilterTest.kt rename to commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/settings/SettingsCatalogFilterTest.kt index 42b0e69dd7..10b234ad70 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsCatalogFilterTest.kt +++ b/commonsUI/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/ui/settings/SettingsCatalogFilterTest.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings +package com.vitorpamplona.amethyst.commons.ui.settings import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.account_settings From 0af3395b582507bdc5ccffe2a3b6726b3b0eb382 Mon Sep 17 00:00:00 2001 From: davotoula <david.kaspar@gmail.com> Date: Thu, 24 Sep 2026 12:04:13 +0200 Subject: [PATCH 39/60] fix(audio): stop dropping all but 2 spectrum frames per decoder buffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pcm tap emits every fft frame of a decoder buffer synchronously from the audio thread, with no suspension point in between, while the UI collector sits on the main dispatcher and cannot interleave. The per-media flow was built with `replay = 1, extraBufferCapacity = 1` and published via `tryEmit`, which on a SUSPEND-overflow flow discards rather than blocks. The buffer therefore filled at 2 and the rest of every burst was thrown away — a hard cap of two frames per decoder buffer no matter how much audio it carried. The visualizer updated at the decoder-buffer rate (~5 Hz) instead of the ~43 Hz the fft actually produces, which is why every live style (bars, waves, radial, aurora) felt equally sluggish. Hold a whole burst instead, and drop the stalest frame rather than the newest if the UI does fall behind. The test pins delivery through the real PcmTapRegistry wiring; before this change it reported 2 frames delivered for bursts of 8, 20 and 50 alike. --- .../playback/playerPool/PcmTapRegistry.kt | 18 +++- .../playerPool/SpectrumBurstDeliveryTest.kt | 86 +++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/playerPool/SpectrumBurstDeliveryTest.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/PcmTapRegistry.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/PcmTapRegistry.kt index 62129db719..5348d81d99 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/PcmTapRegistry.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/PcmTapRegistry.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.commons.audio.Spectrum import com.vitorpamplona.amethyst.commons.audio.normalizeToPeakInPlace import com.vitorpamplona.amethyst.commons.audio.toLogBins import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import java.nio.ByteBuffer @@ -112,6 +113,11 @@ class SpectrumAudioBufferSink( object PcmTapRegistry { private const val MAX_TRACKED_FLOWS = 64 + // Frames buffered per media flow beyond the 1-frame replay. One decoder buffer is typically a + // handful of 1024-sample hops; 63 leaves room for an unusually large one without letting a + // stalled UI bank more than ~1.5 s of stale spectrum. + private const val SPECTRUM_BUFFER_FRAMES = 63 + private val lock = Any() // access-order LinkedHashMap → eldest (least-recently-used) entries iterate first for eviction. @@ -167,7 +173,17 @@ object PcmTapRegistry { if (!fedByLiveSink && !stillCollected) iter.remove() } } - MutableSharedFlow(replay = 1, extraBufferCapacity = 1) + // The audio thread emits every fft frame of a decoder buffer synchronously, with no + // suspension point, while the UI collector sits on the main dispatcher and cannot + // interleave. A 2-slot buffer therefore capped the visualizer at two frames per + // decoder buffer however much audio it carried — the update rate tracked the decoder + // buffer rate (~5 Hz), not the ~43 Hz the fft produces. Hold a whole burst instead, + // and drop the STALEST frame rather than the newest when the UI does fall behind. + MutableSharedFlow( + replay = 1, + extraBufferCapacity = SPECTRUM_BUFFER_FRAMES, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) } } } diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/playerPool/SpectrumBurstDeliveryTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/playerPool/SpectrumBurstDeliveryTest.kt new file mode 100644 index 0000000000..d9c9a48c63 --- /dev/null +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/playerPool/SpectrumBurstDeliveryTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.playback.playerPool + +import androidx.annotation.OptIn +import androidx.media3.common.C +import androidx.media3.common.util.UnstableApi +import com.vitorpamplona.amethyst.commons.audio.Spectrum +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test +import java.nio.ByteBuffer +import java.nio.ByteOrder + +/** + * The audio thread runs [SpectrumAudioBufferSink.handleBuffer] to completion, emitting EVERY fft + * frame of one decoder buffer synchronously with no suspension point in between. The UI collector + * lives on the main dispatcher and cannot interleave with that burst, so anything that does not fit + * in the flow's buffer at that instant is silently dropped by `tryEmit`. + * + * With a 2-slot buffer that capped the visualizer at two frames per decoder buffer no matter how + * much audio it carried, decoupling the update rate from the ~43 Hz the fft actually produces. + */ +@kotlin.OptIn(ExperimentalCoroutinesApi::class) +@OptIn(UnstableApi::class) +class SpectrumBurstDeliveryTest { + private val fftSize = 64 + + private fun monoPcm(totalSamples: Int): ByteBuffer { + val bb = ByteBuffer.allocate(totalSamples * 2).order(ByteOrder.LITTLE_ENDIAN) + for (i in 0 until totalSamples) bb.putShort(((i % 100) * 300).toShort()) + bb.flip() + return bb + } + + /** Frames the UI receives from ONE decoder buffer holding [framesInBurst] fft frames. */ + private fun deliveredFromOneBurst(framesInBurst: Int): Int { + var count = -1 + runTest { + val mediaId = "test://burst-$framesInBurst" + val sink = SpectrumAudioBufferSink(fftSize = fftSize, binCount = 16) + PcmTapRegistry.bind(mediaId, sink) + sink.flush(48000, 1, C.ENCODING_PCM_16BIT) + + val received = mutableListOf<Spectrum>() + val collector = launch { PcmTapRegistry.spectrumFor(mediaId).collect { received.add(it) } } + advanceUntilIdle() // let the collector subscribe + + sink.handleBuffer(monoPcm(fftSize * framesInBurst)) + + advanceUntilIdle() // now let the UI collector run + collector.cancel() + PcmTapRegistry.bind(null, sink) + count = received.size + } + return count + } + + @Test + fun everyFrameOfADecoderBurstReachesTheVisualizer() { + assertEquals("burst of 8", 8, deliveredFromOneBurst(8)) + assertEquals("burst of 20", 20, deliveredFromOneBurst(20)) + assertEquals("burst of 50", 50, deliveredFromOneBurst(50)) + } +} From 65df16bbc3a8abf9a77d847aaa366b0e52b542c6 Mon Sep 17 00:00:00 2001 From: davotoula <david.kaspar@gmail.com> Date: Thu, 24 Sep 2026 12:04:23 +0200 Subject: [PATCH 40/60] fix(audio): pace spectrum frames to the display instead of drawing a burst once Buffering the tap fixes delivery but not motion. Frames still arrive in decoder-sized bursts that run ahead of the audio, and writing a whole burst into Compose state collapses it: every write lands before the next vsync, so the burst draws ONCE and shows only its newest frame. The visual would keep stepping at the decoder-buffer rate despite every frame now arriving. Queue the frames and take exactly one per displayed frame. Production (~43 Hz) is slower than the display, so the queue drains and sits near empty; when it is starved the caller simply holds the frame already drawn rather than redrawing identical bins, so this adds no redundant draws over the previous behaviour. SpectrumPacer is a plain class in commons rather than a flow operator so the queue and eviction rules are unit-testable without a Compose or coroutine harness. It is bounded and evicts the stalest frame so a stalled UI cannot bank a growing backlog and drift permanently behind the audio. --- .../amethyst/commons/audio/SpectrumPacer.kt | 59 ++++++++++++++++ .../commons/audio/SpectrumPacerTest.kt | 67 +++++++++++++++++++ .../amethyst/commons/audio/SpectrumCanvas.kt | 15 ++++- 3 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumPacer.kt create mode 100644 commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumPacerTest.kt diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumPacer.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumPacer.kt new file mode 100644 index 0000000000..bc8fc2c913 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumPacer.kt @@ -0,0 +1,59 @@ +/* + * 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.commons.audio + +/** + * Spreads a bursty spectrum stream over the frames that draw it. + * + * The decoder hands the pcm tap a whole buffer at once, so spectrum frames arrive in bursts that run + * ahead of what is audible (the tap sits upstream of the audio output — see `delayedByFrames`). + * Delivering that burst straight into Compose state collapses it: every write lands before the next + * vsync, so the burst draws ONCE, showing only its newest frame. The visual then steps at the + * decoder-buffer rate instead of the ~43 Hz the fft produces. + * + * Buffering here and taking exactly one frame per drawn frame turns the burst back into motion. + * Production (~43 Hz) is slower than the display (60 Hz+), so the queue drains and sits near empty; + * [next] then returns null and the caller simply holds the frame it already has. + * + * Not thread-safe by design: both ends run on the UI dispatcher. + */ +class SpectrumPacer( + private val capacity: Int = DEFAULT_CAPACITY, +) { + private val queue = ArrayDeque<Spectrum>(capacity) + + /** Queues a freshly decoded frame, evicting the stalest if the UI has fallen behind. */ + fun offer(frame: Spectrum) { + // Bound the queue so a stalled UI cannot bank an ever-growing backlog of stale spectrum and + // drift permanently behind the audio. The newest frame is always the one worth keeping. + while (queue.size >= capacity) queue.removeFirst() + queue.addLast(frame) + } + + /** The next frame to draw, or null when the queue is empty and the last frame should persist. */ + fun next(): Spectrum? = queue.removeFirstOrNull() + + companion object { + // ~1.5 s of 1024-sample hops at 44.1 kHz: room for any realistic decoder buffer without + // letting a stalled UI accumulate an unbounded backlog. + const val DEFAULT_CAPACITY = 64 + } +} diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumPacerTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumPacerTest.kt new file mode 100644 index 0000000000..5dbaf18950 --- /dev/null +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumPacerTest.kt @@ -0,0 +1,67 @@ +/* + * 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.commons.audio + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class SpectrumPacerTest { + private fun frame(id: Float) = Spectrum(floatArrayOf(id)) + + private fun idOf(spectrum: Spectrum?) = spectrum?.bins?.first() + + @Test + fun drainsOneFramePerTickInArrivalOrder() { + val pacer = SpectrumPacer(capacity = 8) + pacer.offer(frame(1f)) + pacer.offer(frame(2f)) + pacer.offer(frame(3f)) + + assertEquals(1f, idOf(pacer.next())) + assertEquals(2f, idOf(pacer.next())) + assertEquals(3f, idOf(pacer.next())) + } + + @Test + fun returnsNullWhenStarvedSoTheCallerCanHoldTheLastFrame() { + val pacer = SpectrumPacer(capacity = 8) + assertNull(pacer.next()) + + pacer.offer(frame(1f)) + assertEquals(1f, idOf(pacer.next())) + assertNull(pacer.next()) + } + + @Test + fun overCapacityDropsTheStalestFrameNotTheNewest() { + val pacer = SpectrumPacer(capacity = 3) + pacer.offer(frame(1f)) + pacer.offer(frame(2f)) + pacer.offer(frame(3f)) + pacer.offer(frame(4f)) // evicts 1 + + assertEquals(2f, idOf(pacer.next())) + assertEquals(3f, idOf(pacer.next())) + assertEquals(4f, idOf(pacer.next())) + assertNull(pacer.next()) + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumCanvas.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumCanvas.kt index a3c0b51eca..ae54e3b19f 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumCanvas.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumCanvas.kt @@ -46,9 +46,22 @@ fun SpectrumCanvas( draw: DrawScope.(bins: FloatArray, timeSec: Float, palette: VisualizerPalette) -> Unit, ) { val smoothed = remember { mutableStateOf(FloatArray(0)) } + + // Frames arrive in decoder-sized bursts that run ahead of the audio, so they are queued here and + // drawn one per displayed frame. Writing a whole burst straight into `smoothed` would collapse it + // into a single draw at the next vsync and the visual would step at the decoder-buffer rate. + val pacer = remember(spectrum) { SpectrumPacer() } + LaunchedEffect(spectrum) { + spectrum.collect { pacer.offer(it) } + } + LaunchedEffect(spectrum, decay) { var prev = FloatArray(0) - spectrum.collect { frame -> + while (true) { + withFrameMillis { } + // Starved: production (~43 Hz) is slower than the display, so this is the common case + // between frames. Hold what is already drawn rather than redrawing the same bins. + val frame = pacer.next() ?: continue // A fresh array each frame is intentional: mutableStateOf compares by reference, so a new // instance is what signals Compose to redraw. Do NOT switch to in-place mutation. val next = From 22051c546d7c1d94bdcb2a9e60b1007ff586fc65 Mon Sep 17 00:00:00 2001 From: davotoula <david.kaspar@gmail.com> Date: Thu, 24 Sep 2026 12:23:33 +0200 Subject: [PATCH 41/60] refactor(audio): extract the per-frame visualizer step into a testable SpectrumTrail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pacing commit left the queue drain, the decay trail and the fresh-array rule inline in SpectrumCanvas, where none of it could be covered — commonsUI has no Compose test harness, and adding one to a KMP module for three lines of glue is not worth the dependency. Move the whole per-displayed-frame step into SpectrumTrail in commons instead: take one queued frame, decay against what is drawn, return a new array, or null when starved. The composable keeps only the plumbing (collect into offer, drain on withFrameMillis), so what is left untested is code with no branches. Covered: starvation holds the drawn frame, the first frame has nothing to decay from, a rising bin attacks instantly, a falling bin releases gradually, each draw gets a distinct array (mutableStateOf compares by reference, so reuse would silently stop redrawing), and a backlog evicts the stalest frame. Also corrects the SpectrumCanvas KDoc: there is now a per-frame loop whatever `animated` is, so the note about it existing to avoid 60fps redraws no longer described the code. It only governs the monotonic clock. --- .../amethyst/commons/audio/SpectrumTrail.kt | 63 ++++++++++++ .../commons/audio/SpectrumTrailTest.kt | 96 +++++++++++++++++++ .../amethyst/commons/audio/SpectrumCanvas.kt | 40 ++++---- 3 files changed, 178 insertions(+), 21 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrail.kt create mode 100644 commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrailTest.kt diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrail.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrail.kt new file mode 100644 index 0000000000..e4bb934759 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrail.kt @@ -0,0 +1,63 @@ +/* + * 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.commons.audio + +/** + * The whole per-displayed-frame step of the spectrum visualizer: take one queued frame, apply the + * decay trail against what is already on screen, and return a fresh array to draw. + * + * Bursty spectrum frames are paced through a [SpectrumPacer] (see its docs for why), and the decay + * gives each bin an instant attack and a gradual release, so bars snap up to a transient and fall + * back smoothly instead of flickering. + * + * This lives here, rather than inline in the Compose collector, so the queueing, starvation and + * decay rules are unit-testable without a Compose harness; the composable is left as plumbing. + * + * Not thread-safe by design: both ends run on the UI dispatcher. + */ +class SpectrumTrail( + private val decay: Float, + capacity: Int = SpectrumPacer.DEFAULT_CAPACITY, +) { + private val pacer = SpectrumPacer(capacity) + private var drawn = FloatArray(0) + + /** Queues a freshly decoded frame, evicting the stalest if the UI has fallen behind. */ + fun offer(frame: Spectrum) = pacer.offer(frame) + + /** + * The next array to draw, or null when no frame is queued and the current one should persist. + * + * A fresh array each time is intentional: `mutableStateOf` compares by reference, so a new + * instance is what signals Compose to redraw. Do NOT switch to in-place mutation. + */ + fun nextOrNull(): FloatArray? { + val frame = pacer.next() ?: return null + val prev = drawn + val next = + FloatArray(frame.bins.size) { i -> + val prior = if (i < prev.size) prev[i] * decay else 0f + if (frame.bins[i] > prior) frame.bins[i] else prior + } + drawn = next + return next + } +} diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrailTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrailTest.kt new file mode 100644 index 0000000000..9708e33105 --- /dev/null +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrailTest.kt @@ -0,0 +1,96 @@ +/* + * 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.commons.audio + +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertNotSame +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * The whole per-displayed-frame step the visualizer runs: take one queued frame, apply the decay + * trail against what is already drawn, and hand back a fresh array. Extracted from SpectrumCanvas + * so it is testable without a Compose harness — the composable is left as plumbing around this. + */ +class SpectrumTrailTest { + private fun frame(vararg bins: Float) = Spectrum(bins) + + @Test + fun yieldsNothingWhenStarvedSoTheDrawnFrameIsHeld() { + val trail = SpectrumTrail(decay = 0.5f) + assertNull(trail.nextOrNull()) + } + + @Test + fun theFirstFrameIsDrawnAsIsWithNothingToDecayFrom() { + val trail = SpectrumTrail(decay = 0.5f) + trail.offer(frame(1f, 0.5f, 0f)) + + assertContentEquals(floatArrayOf(1f, 0.5f, 0f), trail.nextOrNull()) + } + + @Test + fun aRisingBinTakesItsNewValueImmediately() { + val trail = SpectrumTrail(decay = 0.5f) + trail.offer(frame(0.2f)) + trail.nextOrNull() + trail.offer(frame(0.9f)) + + assertContentEquals(floatArrayOf(0.9f), trail.nextOrNull()) + } + + @Test + fun aFallingBinDecaysFromWhatWasDrawnRatherThanSnapping() { + val trail = SpectrumTrail(decay = 0.5f) + trail.offer(frame(1f)) + trail.nextOrNull() + trail.offer(frame(0f)) + + // 1f * 0.5 decay beats the new 0f, so the bar falls gradually. + assertContentEquals(floatArrayOf(0.5f), trail.nextOrNull()) + } + + @Test + fun eachDrawGetsAFreshArraySoComposeSeesTheChange() { + val trail = SpectrumTrail(decay = 0.5f) + trail.offer(frame(1f)) + trail.offer(frame(1f)) + + val first = trail.nextOrNull() + val second = trail.nextOrNull() + + // mutableStateOf compares by reference; reusing one array would never trigger a redraw. + assertNotSame(first, second) + } + + @Test + fun aBacklogIsBoundedByDroppingTheStalestFrame() { + val trail = SpectrumTrail(decay = 0f, capacity = 2) + trail.offer(frame(1f)) + trail.offer(frame(2f)) + trail.offer(frame(3f)) // evicts the 1f frame + + assertContentEquals(floatArrayOf(2f), trail.nextOrNull()) + assertContentEquals(floatArrayOf(3f), trail.nextOrNull()) + assertTrue(trail.nextOrNull() == null) + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumCanvas.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumCanvas.kt index ae54e3b19f..1326ffac57 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumCanvas.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumCanvas.kt @@ -34,7 +34,14 @@ import kotlinx.coroutines.flow.Flow * Collects [spectrum] into a decayed [FloatArray] and optionally runs a monotonic time clock, * then calls [draw] inside the Canvas draw lambda. The fast-changing state is read * ONLY in the draw lambda, so new frames trigger the draw phase, never recomposition. - * Pass [animated] = false for non-time-varying styles (bars, radial) to avoid 60fps redraws. + * + * Frames are paced one per displayed frame through [SpectrumTrail] — they arrive from the decoder in + * bursts, and writing a burst straight into state would collapse it into a single draw. The pacing + * loop runs every frame but only writes state when a spectrum frame is actually queued, so the redraw + * rate still tracks the ~43 Hz the fft produces rather than the display. + * + * Pass [animated] = false for non-time-varying styles (bars, radial): that drops the monotonic clock, + * whose whole purpose is to redraw every frame even when the spectrum has not moved. */ @Composable fun SpectrumCanvas( @@ -47,30 +54,21 @@ fun SpectrumCanvas( ) { val smoothed = remember { mutableStateOf(FloatArray(0)) } - // Frames arrive in decoder-sized bursts that run ahead of the audio, so they are queued here and - // drawn one per displayed frame. Writing a whole burst straight into `smoothed` would collapse it - // into a single draw at the next vsync and the visual would step at the decoder-buffer rate. - val pacer = remember(spectrum) { SpectrumPacer() } - LaunchedEffect(spectrum) { - spectrum.collect { pacer.offer(it) } + // Frames arrive in decoder-sized bursts that run ahead of the audio, so they are queued and drawn + // one per displayed frame. Writing a whole burst straight into `smoothed` would collapse it into a + // single draw at the next vsync and the visual would step at the decoder-buffer rate. Queueing and + // decay live in SpectrumTrail so they are testable without a Compose harness. + val trail = remember(spectrum, decay) { SpectrumTrail(decay) } + LaunchedEffect(spectrum, trail) { + spectrum.collect { trail.offer(it) } } - LaunchedEffect(spectrum, decay) { - var prev = FloatArray(0) + LaunchedEffect(trail) { while (true) { withFrameMillis { } - // Starved: production (~43 Hz) is slower than the display, so this is the common case - // between frames. Hold what is already drawn rather than redrawing the same bins. - val frame = pacer.next() ?: continue - // A fresh array each frame is intentional: mutableStateOf compares by reference, so a new - // instance is what signals Compose to redraw. Do NOT switch to in-place mutation. - val next = - FloatArray(frame.bins.size) { i -> - val prior = if (i < prev.size) prev[i] * decay else 0f - if (frame.bins[i] > prior) frame.bins[i] else prior - } - smoothed.value = next - prev = next + // Null means starved. Production (~43 Hz) is slower than the display, so this is the common + // case between frames: hold what is drawn rather than redrawing identical bins. + smoothed.value = trail.nextOrNull() ?: continue } } From 991687a33e563c579a24c19c729fb695593611de Mon Sep 17 00:00:00 2001 From: davotoula <david.kaspar@gmail.com> Date: Thu, 24 Sep 2026 14:47:34 +0200 Subject: [PATCH 42/60] docs(playback): correct the log-level claims guarding the diagnostic traces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both comments described levels no variant actually uses. DEFAULT_LOG_LEVEL gives debug builds INFO (not DEBUG) and release WARN (not ERROR), and the `benchmark` build type counts as `isDebug`, so it gets INFO too rather than being the silent release-like variant the text implied. The practical consequence was the misleading part: both said a debug build is enough to capture the trace. It is not — these are `Log.d`, which needs `minLevel <= DEBUG`, so PlaybackDiag and VideoQuality are silent in a stock debug build and only appear once Amethyst.VERBOSE_LOGS is flipped to true. RelayUsageListener already documents this correctly ("a debug build defaults to LogLevel.INFO"); these two had drifted from it. --- .../amethyst/service/playback/PlaybackDiag.kt | 10 +++++++--- .../composable/controls/VideoQualityControls.kt | 8 +++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackDiag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackDiag.kt index c6cd9c9085..ac5c018225 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackDiag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackDiag.kt @@ -22,9 +22,13 @@ package com.vitorpamplona.amethyst.service.playback /** * Shared logcat tag for the playback diagnostic trace (source routing, player lifecycle, error - * recovery, HLS liveness learning). Emitted with `Log.d`, so it appears only in a debug build - * (`Log.minLevel = DEBUG`) and is silent in benchmark/release (`ERROR`). To capture a playback - * investigation, install a debug build and run: + * recovery, HLS liveness learning). + * + * Emitted with `Log.d`, which fires only while `Log.minLevel <= DEBUG`. No shipped variant is + * there by default: [com.vitorpamplona.amethyst.Amethyst.DEFAULT_LOG_LEVEL] gives debug AND + * benchmark builds `INFO` (the `benchmark` build type counts as `isDebug`) and release `WARN`, so + * this trace is silent everywhere until `Amethyst.VERBOSE_LOGS` is flipped to true. To capture a + * playback investigation, set that flag, install a debug build and run: * * ``` * adb logcat -s PlaybackDiag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/VideoQualityControls.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/VideoQualityControls.kt index dbc3491464..8acee68d0a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/VideoQualityControls.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/VideoQualityControls.kt @@ -148,9 +148,11 @@ const val VIDEO_QUALITY_TAG = "VideoQuality" * Traces which rendition adaptive selection landed on, against the full ladder the manifest * offered. * - * The listener is registered only when the trace can actually be emitted — debug builds set - * `Log.minLevel = DEBUG` while benchmark/release set `ERROR` (see [PLAYBACK_DIAG_TAG]) — so the - * release path keeps the "no listener per player" property that dropping the old selector bought. + * The listener is registered only when the trace can actually be emitted. `Log.minLevel` is above + * `DEBUG` in every variant by default — `INFO` for debug and benchmark builds, `WARN` for release + * (see [PLAYBACK_DIAG_TAG]) — so this costs nothing until `Amethyst.VERBOSE_LOGS` is turned on, + * and every build keeps the "no listener per player" property that dropping the old selector + * bought. */ @Composable fun LogVideoQualitySelection(player: Player) { From 33f684773b86c439cc0bd8c845d0467540f2da14 Mon Sep 17 00:00:00 2001 From: davotoula <david.kaspar@gmail.com> Date: Sun, 27 Sep 2026 08:37:37 +0200 Subject: [PATCH 43/60] fix(audio): pace visualizer frames by the audio time they cover, not the display refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Releasing one spectrum frame per display refresh still looked jerky on device: bursts of fast motion with regular freezes two or three times a second. Measured on a Pixel 9a at 120 Hz with temporary logging: frames reach the UI in clusters of ~15 roughly every 330 ms, because the audio pipeline fills its output buffer in chunks. Released one per 8.3 ms refresh, a cluster played out in ~110 ms (3x too fast) and the screen then froze for ~220 ms until the next one — 161 stalls over 52.8 s, 3.05 a second, averaging 211 ms. Each Spectrum now carries the audio time it describes (fft size / sample rate, ~21 ms at 48 kHz) and SpectrumTrail releases a frame only once the previous one has covered its time, so a cluster spreads across the gap to the next. Time lost while starved is not owed back: after a gap, frames resume from the current frame time rather than being dumped at once to catch up. The backlog is capped at 24 frames (~0.5 s) so 2x playback speed cannot leave the picture drifting behind the sound; ordinary clusters peak at ~15 and are never trimmed. The duration defaults to zero, meaning "show on arrival", so producers that do not pace to audio — the synthetic preview emits one frame per display frame — keep their existing behaviour. Same device afterwards: 46.5 frames/s (48 kHz / 1024 = 46.9), median gap 24 ms, p95 26 ms, 2 gaps over 150 ms in 49.3 s, peak backlog 15. Also corrects the PcmTapRegistry comment from the buffering fix, which blamed a single large decoder buffer. The logs show decoder calls mostly carry one fft frame; the burst is many calls landing within a few ms, which the main-thread collector cannot interleave with either way. --- .../playback/playerPool/PcmTapRegistry.kt | 24 ++-- .../playerPool/SpectrumAudioBufferSinkTest.kt | 25 ++++ .../amethyst/commons/audio/AudioSpectrum.kt | 9 +- .../amethyst/commons/audio/SpectrumPacer.kt | 18 ++- .../amethyst/commons/audio/SpectrumTrail.kt | 63 ++++++-- .../commons/audio/SpectrumTrailTest.kt | 134 ++++++++++++++---- .../amethyst/commons/audio/SpectrumCanvas.kt | 25 ++-- 7 files changed, 226 insertions(+), 72 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/PcmTapRegistry.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/PcmTapRegistry.kt index 5348d81d99..70b32c1a03 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/PcmTapRegistry.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/PcmTapRegistry.kt @@ -66,6 +66,10 @@ class SpectrumAudioBufferSink( private var channels = 1 private var encoding = C.ENCODING_PCM_16BIT + // Audio time one fft frame covers: fftSize samples per channel at the stream's sample rate. + // Zero until the first flush reports a rate, which leaves the frame unpaced rather than wrong. + private var frameDurationNanos = 0L + @kotlin.OptIn(ExperimentalCoroutinesApi::class) override fun flush( sampleRateHz: Int, @@ -74,6 +78,7 @@ class SpectrumAudioBufferSink( ) { this.channels = channelCount.coerceAtLeast(1) this.encoding = encoding + this.frameDurationNanos = if (sampleRateHz > 0) fftSize * 1_000_000_000L / sampleRateHz else 0L filled = 0 output?.resetReplayCache() } @@ -99,7 +104,7 @@ class SpectrumAudioBufferSink( // Skip the DC bin (index 0): toLogBins ignores it, so letting a DC/offset component be the // peak would scale every audible bin toward zero and wash the spectrum out. mags.normalizeToPeakInPlace(fromIndex = 1) - output?.tryEmit(Spectrum(mags.toLogBins(binCount))) + output?.tryEmit(Spectrum(mags.toLogBins(binCount), frameDurationNanos)) } } @@ -113,8 +118,8 @@ class SpectrumAudioBufferSink( object PcmTapRegistry { private const val MAX_TRACKED_FLOWS = 64 - // Frames buffered per media flow beyond the 1-frame replay. One decoder buffer is typically a - // handful of 1024-sample hops; 63 leaves room for an unusually large one without letting a + // Frames buffered per media flow beyond the 1-frame replay. A cluster is ~15 fft frames (~0.33 s + // of audio, measured on a Pixel 9a); 63 leaves room for an unusually large one without letting a // stalled UI bank more than ~1.5 s of stale spectrum. private const val SPECTRUM_BUFFER_FRAMES = 63 @@ -173,12 +178,13 @@ object PcmTapRegistry { if (!fedByLiveSink && !stillCollected) iter.remove() } } - // The audio thread emits every fft frame of a decoder buffer synchronously, with no - // suspension point, while the UI collector sits on the main dispatcher and cannot - // interleave. A 2-slot buffer therefore capped the visualizer at two frames per - // decoder buffer however much audio it carried — the update rate tracked the decoder - // buffer rate (~5 Hz), not the ~43 Hz the fft produces. Hold a whole burst instead, - // and drop the STALEST frame rather than the newest when the UI does fall behind. + // The audio thread emits in clusters — the pipeline fills its output buffer ~3 times a + // second, so ~15 fft frames land within a few ms of each other (one per decoder call, + // or several from one large call) — while the UI collector sits on the main dispatcher + // and cannot run in between. A 2-slot buffer therefore kept only ~2 frames of each + // cluster and dropped the rest. Hold a whole cluster instead, and drop the STALEST + // frame rather than the newest if the UI does fall behind. (Spreading the cluster + // back over time is SpectrumTrail's job, not this flow's.) MutableSharedFlow( replay = 1, extraBufferCapacity = SPECTRUM_BUFFER_FRAMES, diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/playerPool/SpectrumAudioBufferSinkTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/playerPool/SpectrumAudioBufferSinkTest.kt index 6e9ce2ea35..012ddf6e72 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/playerPool/SpectrumAudioBufferSinkTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/playerPool/SpectrumAudioBufferSinkTest.kt @@ -136,4 +136,29 @@ class SpectrumAudioBufferSinkTest { assertTrue("non-16-bit PCM must not emit a spectrum", out.replayCache.isEmpty()) } + + // Pacing draws each frame for the audio time it covers, so the sink has to say how long that is. + // It is fftSize samples PER CHANNEL: a stereo stream must not report half (or double) the time. + @Test + fun eachFrameCarriesTheAudioTimeItCovers() { + val sink = SpectrumAudioBufferSink(fftSize = fftSize, binCount = binCount) + val out = MutableSharedFlow<Spectrum>(replay = 1, extraBufferCapacity = 1) + sink.output = out + sink.flush(48000, 1, C.ENCODING_PCM_16BIT) + sink.handleBuffer(monoPcm(sineShorts(k = 2, n = fftSize))) + + assertEquals(fftSize * 1_000_000_000L / 48000, out.replayCache.last().durationNanos) + } + + @Test + fun stereoFramesCoverTheSameTimeAsMono() { + val sink = SpectrumAudioBufferSink(fftSize = fftSize, binCount = binCount) + val out = MutableSharedFlow<Spectrum>(replay = 1, extraBufferCapacity = 1) + sink.output = out + sink.flush(44100, 2, C.ENCODING_PCM_16BIT) + val tone = sineShorts(k = 2, n = fftSize) + sink.handleBuffer(interleavedStereoPcm(tone, tone)) + + assertEquals(fftSize * 1_000_000_000L / 44100, out.replayCache.last().durationNanos) + } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/AudioSpectrum.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/AudioSpectrum.kt index 86eb82822c..c074eecc49 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/AudioSpectrum.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/AudioSpectrum.kt @@ -26,9 +26,16 @@ import kotlin.math.exp import kotlin.math.ln import kotlin.math.log10 -/** One frame of frequency-domain magnitudes, ordered low→high Hz and normalized 0f..1f. */ +/** + * One frame of frequency-domain magnitudes, ordered low→high Hz and normalized 0f..1f. + * + * [durationNanos] is how much audio the frame describes (fft size / sample rate), which is how long + * it should stay on screen. Zero — the default — means the producer is not pacing to audio (the + * synthetic preview emits one per display frame), so the frame is shown as soon as it arrives. + */ class Spectrum( val bins: FloatArray, + val durationNanos: Long = 0L, ) /** diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumPacer.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumPacer.kt index bc8fc2c913..e6f8cc2e1d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumPacer.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumPacer.kt @@ -21,17 +21,13 @@ package com.vitorpamplona.amethyst.commons.audio /** - * Spreads a bursty spectrum stream over the frames that draw it. + * The bounded queue between the audio tap and the visualizer. * - * The decoder hands the pcm tap a whole buffer at once, so spectrum frames arrive in bursts that run - * ahead of what is audible (the tap sits upstream of the audio output — see `delayedByFrames`). - * Delivering that burst straight into Compose state collapses it: every write lands before the next - * vsync, so the burst draws ONCE, showing only its newest frame. The visual then steps at the - * decoder-buffer rate instead of the ~43 Hz the fft produces. - * - * Buffering here and taking exactly one frame per drawn frame turns the burst back into motion. - * Production (~43 Hz) is slower than the display (60 Hz+), so the queue drains and sits near empty; - * [next] then returns null and the caller simply holds the frame it already has. + * Frames reach the UI in clusters, because the audio pipeline fills its output buffer in chunks + * (~15 frames about three times a second, measured on a Pixel 9a). This holds them until + * [SpectrumTrail] releases each one when its audio time comes due. It is bounded and evicts the + * stalest frame, so faster-than-real-time playback or a stalled UI cannot bank an ever-growing + * backlog and leave the picture permanently behind the sound. * * Not thread-safe by design: both ends run on the UI dispatcher. */ @@ -48,6 +44,8 @@ class SpectrumPacer( queue.addLast(frame) } + fun isEmpty(): Boolean = queue.isEmpty() + /** The next frame to draw, or null when the queue is empty and the last frame should persist. */ fun next(): Spectrum? = queue.removeFirstOrNull() diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrail.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrail.kt index e4bb934759..98e40f7770 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrail.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrail.kt @@ -21,36 +21,68 @@ package com.vitorpamplona.amethyst.commons.audio /** - * The whole per-displayed-frame step of the spectrum visualizer: take one queued frame, apply the - * decay trail against what is already on screen, and return a fresh array to draw. + * The whole per-displayed-frame step of the spectrum visualizer: release queued frames as their + * audio time comes due, apply the decay trail against what is already on screen, and return a + * fresh array to draw. * - * Bursty spectrum frames are paced through a [SpectrumPacer] (see its docs for why), and the decay - * gives each bin an instant attack and a gradual release, so bars snap up to a transient and fall - * back smoothly instead of flickering. + * Frames arrive in clusters (see [SpectrumPacer]). Releasing one per display refresh drained a + * ~15-frame cluster in ~110 ms at 120 Hz and then froze until the next one — measured as ~3 stalls + * of ~210 ms every second. Each frame instead stays up for the [Spectrum.durationNanos] of audio it + * describes, so a cluster spreads across the gap to the next. Time lost while starved is not owed + * back: when frames return after a gap they resume from the current frame time rather than being + * dumped at once to catch up. * - * This lives here, rather than inline in the Compose collector, so the queueing, starvation and - * decay rules are unit-testable without a Compose harness; the composable is left as plumbing. + * The decay gives each bin an instant attack and a gradual release, so bars snap up to a transient + * and fall back smoothly instead of flickering. + * + * This lives here, rather than inline in the Compose collector, so the pacing, starvation and decay + * rules are unit-testable without a Compose harness; the composable is left as plumbing. * * Not thread-safe by design: both ends run on the UI dispatcher. */ class SpectrumTrail( private val decay: Float, - capacity: Int = SpectrumPacer.DEFAULT_CAPACITY, + capacity: Int = MAX_BACKLOG_FRAMES, ) { private val pacer = SpectrumPacer(capacity) private var drawn = FloatArray(0) - /** Queues a freshly decoded frame, evicting the stalest if the UI has fallen behind. */ + // When the frame at the head of the queue may be drawn, in the caller's frame-clock nanos. + private var dueNanos = Long.MIN_VALUE + private var starved = true + + /** Queues a freshly decoded frame, evicting the stalest if the backlog is at capacity. */ fun offer(frame: Spectrum) = pacer.offer(frame) /** - * The next array to draw, or null when no frame is queued and the current one should persist. + * The array to draw at [frameTimeNanos], or null when nothing new is due and the current one + * should persist. * * A fresh array each time is intentional: `mutableStateOf` compares by reference, so a new * instance is what signals Compose to redraw. Do NOT switch to in-place mutation. */ - fun nextOrNull(): FloatArray? { - val frame = pacer.next() ?: return null + fun nextOrNull(frameTimeNanos: Long): FloatArray? { + if (pacer.isEmpty()) { + starved = true + return null + } + if (starved) { + // Resume from now, but never earlier than the last drawn frame's audio time runs out. + dueNanos = maxOf(dueNanos, frameTimeNanos) + starved = false + } + + var result: FloatArray? = null + while (dueNanos <= frameTimeNanos) { + val frame = pacer.next() ?: break + result = decayedFrom(frame) + dueNanos += frame.durationNanos + } + + return result + } + + private fun decayedFrom(frame: Spectrum): FloatArray { val prev = drawn val next = FloatArray(frame.bins.size) { i -> @@ -60,4 +92,11 @@ class SpectrumTrail( drawn = next return next } + + companion object { + // A cluster is ~15 frames, so 24 (~0.5 s of audio) never trims ordinary playback, while + // capping how far the picture can lag the sound when frames come faster than real time + // (2x playback speed). + const val MAX_BACKLOG_FRAMES = 24 + } } diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrailTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrailTest.kt index 9708e33105..39a73cb261 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrailTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrailTest.kt @@ -24,49 +24,139 @@ import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertNotSame import kotlin.test.assertNull -import kotlin.test.assertTrue /** - * The whole per-displayed-frame step the visualizer runs: take one queued frame, apply the decay - * trail against what is already drawn, and hand back a fresh array. Extracted from SpectrumCanvas - * so it is testable without a Compose harness — the composable is left as plumbing around this. + * The per-displayed-frame step of the visualizer: release queued frames as their audio time comes + * due, apply the decay trail against what is drawn, and hand back a fresh array. + * + * Frames reach the UI in clusters (~15 at a time, ~3 times a second, measured on a Pixel 9a) because + * the audio pipeline fills its output buffer in chunks. Releasing one per display refresh drained a + * cluster in ~110 ms at 120 Hz and then froze for ~220 ms. Frames must instead be released at the + * rate of the audio they describe, so a cluster spreads across the gap to the next one. */ class SpectrumTrailTest { - private fun frame(vararg bins: Float) = Spectrum(bins) + private val ms = 1_000_000L + + private fun frame( + value: Float, + durationMs: Long = 20, + ) = Spectrum(floatArrayOf(value), durationNanos = durationMs * ms) @Test fun yieldsNothingWhenStarvedSoTheDrawnFrameIsHeld() { val trail = SpectrumTrail(decay = 0.5f) - assertNull(trail.nextOrNull()) + assertNull(trail.nextOrNull(0)) } @Test - fun theFirstFrameIsDrawnAsIsWithNothingToDecayFrom() { + fun theFirstFrameIsDrawnOnArrivalWithNothingToDecayFrom() { val trail = SpectrumTrail(decay = 0.5f) - trail.offer(frame(1f, 0.5f, 0f)) + trail.offer(frame(1f)) - assertContentEquals(floatArrayOf(1f, 0.5f, 0f), trail.nextOrNull()) + assertContentEquals(floatArrayOf(1f), trail.nextOrNull(0)) + } + + @Test + fun aClusterIsSpreadOverTheAudioTimeItCoversNotTheScreenRefresh() { + val trail = SpectrumTrail(decay = 0f) + trail.offer(frame(1f)) + trail.offer(frame(2f)) + trail.offer(frame(3f)) + + // 120 Hz refreshes (~8 ms) must not drain a frame that covers 20 ms of audio. + assertContentEquals(floatArrayOf(1f), trail.nextOrNull(0)) + assertNull(trail.nextOrNull(8 * ms)) + assertNull(trail.nextOrNull(16 * ms)) + assertContentEquals(floatArrayOf(2f), trail.nextOrNull(20 * ms)) + assertNull(trail.nextOrNull(33 * ms)) + assertContentEquals(floatArrayOf(3f), trail.nextOrNull(40 * ms)) + assertNull(trail.nextOrNull(48 * ms)) + } + + @Test + fun afterStarvingTheNextClusterStartsWhenItArrivesInsteadOfBeingDumpedToCatchUp() { + val trail = SpectrumTrail(decay = 0f) + trail.offer(frame(1f)) + assertContentEquals(floatArrayOf(1f), trail.nextOrNull(0)) + assertNull(trail.nextOrNull(100 * ms)) // starved for a while + + trail.offer(frame(2f)) + trail.offer(frame(3f)) + + // The idle time is not owed: frame 2 shows now and frame 3 a full frame later. + assertContentEquals(floatArrayOf(2f), trail.nextOrNull(300 * ms)) + assertNull(trail.nextOrNull(308 * ms)) + assertContentEquals(floatArrayOf(3f), trail.nextOrNull(320 * ms)) + } + + @Test + fun aFrameArrivingBeforeThePreviousOneElapsedStillWaitsItsTurn() { + val trail = SpectrumTrail(decay = 0f) + trail.offer(frame(1f)) + assertContentEquals(floatArrayOf(1f), trail.nextOrNull(0)) + assertNull(trail.nextOrNull(8 * ms)) // queue momentarily empty + + trail.offer(frame(2f)) + + assertNull(trail.nextOrNull(16 * ms)) // frame 1 still covers until 20 ms + assertContentEquals(floatArrayOf(2f), trail.nextOrNull(24 * ms)) + } + + @Test + fun aSlowFrameClockJumpsToTheLatestDueFrame() { + val trail = SpectrumTrail(decay = 0f) + trail.offer(frame(1f)) + trail.offer(frame(2f)) + trail.offer(frame(3f)) + + assertContentEquals(floatArrayOf(1f), trail.nextOrNull(0)) + // A janky 45 ms frame: frames 2 (due 20) and 3 (due 40) are both due; show the newest. + assertContentEquals(floatArrayOf(3f), trail.nextOrNull(45 * ms)) + } + + @Test + fun aFrameWithNoDurationIsShownOnArrival() { + // Producers that do not declare a duration (the synthetic preview emits one per display + // frame) keep the old behaviour: whatever is queued is current. + val trail = SpectrumTrail(decay = 0f) + trail.offer(Spectrum(floatArrayOf(1f))) + trail.offer(Spectrum(floatArrayOf(2f))) + + assertContentEquals(floatArrayOf(2f), trail.nextOrNull(0)) + } + + @Test + fun aBacklogIsCappedByDroppingTheStalestFrame() { + // Faster-than-real-time playback (2x speed) produces frames faster than they come due; + // the cap bounds how far the picture can lag the sound. + val trail = SpectrumTrail(decay = 0f, capacity = 2) + trail.offer(frame(1f)) + trail.offer(frame(2f)) + trail.offer(frame(3f)) // evicts frame 1 + + assertContentEquals(floatArrayOf(2f), trail.nextOrNull(0)) + assertContentEquals(floatArrayOf(3f), trail.nextOrNull(20 * ms)) + assertNull(trail.nextOrNull(40 * ms)) } @Test fun aRisingBinTakesItsNewValueImmediately() { val trail = SpectrumTrail(decay = 0.5f) trail.offer(frame(0.2f)) - trail.nextOrNull() + trail.nextOrNull(0) trail.offer(frame(0.9f)) - assertContentEquals(floatArrayOf(0.9f), trail.nextOrNull()) + assertContentEquals(floatArrayOf(0.9f), trail.nextOrNull(20 * ms)) } @Test fun aFallingBinDecaysFromWhatWasDrawnRatherThanSnapping() { val trail = SpectrumTrail(decay = 0.5f) trail.offer(frame(1f)) - trail.nextOrNull() + trail.nextOrNull(0) trail.offer(frame(0f)) - // 1f * 0.5 decay beats the new 0f, so the bar falls gradually. - assertContentEquals(floatArrayOf(0.5f), trail.nextOrNull()) + assertContentEquals(floatArrayOf(0.5f), trail.nextOrNull(20 * ms)) } @Test @@ -75,22 +165,10 @@ class SpectrumTrailTest { trail.offer(frame(1f)) trail.offer(frame(1f)) - val first = trail.nextOrNull() - val second = trail.nextOrNull() + val first = trail.nextOrNull(0) + val second = trail.nextOrNull(20 * ms) // mutableStateOf compares by reference; reusing one array would never trigger a redraw. assertNotSame(first, second) } - - @Test - fun aBacklogIsBoundedByDroppingTheStalestFrame() { - val trail = SpectrumTrail(decay = 0f, capacity = 2) - trail.offer(frame(1f)) - trail.offer(frame(2f)) - trail.offer(frame(3f)) // evicts the 1f frame - - assertContentEquals(floatArrayOf(2f), trail.nextOrNull()) - assertContentEquals(floatArrayOf(3f), trail.nextOrNull()) - assertTrue(trail.nextOrNull() == null) - } } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumCanvas.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumCanvas.kt index 1326ffac57..56a2ec66d9 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumCanvas.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumCanvas.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.withFrameMillis +import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.drawscope.DrawScope import kotlinx.coroutines.flow.Flow @@ -35,10 +36,10 @@ import kotlinx.coroutines.flow.Flow * then calls [draw] inside the Canvas draw lambda. The fast-changing state is read * ONLY in the draw lambda, so new frames trigger the draw phase, never recomposition. * - * Frames are paced one per displayed frame through [SpectrumTrail] — they arrive from the decoder in - * bursts, and writing a burst straight into state would collapse it into a single draw. The pacing - * loop runs every frame but only writes state when a spectrum frame is actually queued, so the redraw - * rate still tracks the ~43 Hz the fft produces rather than the display. + * Frames are released through [SpectrumTrail] as the audio they describe comes due — they arrive + * in clusters, and neither dumping a cluster into state nor draining it one per vsync looks live. + * The pacing loop runs every frame but only writes state when a frame is due, so the redraw rate + * tracks the ~43-47 Hz the fft produces rather than the display. * * Pass [animated] = false for non-time-varying styles (bars, radial): that drops the monotonic clock, * whose whole purpose is to redraw every frame even when the spectrum has not moved. @@ -54,10 +55,10 @@ fun SpectrumCanvas( ) { val smoothed = remember { mutableStateOf(FloatArray(0)) } - // Frames arrive in decoder-sized bursts that run ahead of the audio, so they are queued and drawn - // one per displayed frame. Writing a whole burst straight into `smoothed` would collapse it into a - // single draw at the next vsync and the visual would step at the decoder-buffer rate. Queueing and - // decay live in SpectrumTrail so they are testable without a Compose harness. + // Frames arrive in clusters, so they are queued and released as their audio time comes due. + // Writing a cluster straight into `smoothed` collapses it into one draw; releasing one per vsync + // races through it and then freezes. Pacing and decay live in SpectrumTrail so they are testable + // without a Compose harness. val trail = remember(spectrum, decay) { SpectrumTrail(decay) } LaunchedEffect(spectrum, trail) { spectrum.collect { trail.offer(it) } @@ -65,10 +66,10 @@ fun SpectrumCanvas( LaunchedEffect(trail) { while (true) { - withFrameMillis { } - // Null means starved. Production (~43 Hz) is slower than the display, so this is the common - // case between frames: hold what is drawn rather than redrawing identical bins. - smoothed.value = trail.nextOrNull() ?: continue + val frameTimeNanos = withFrameNanos { it } + // Null means nothing new is due. Frames cover ~21 ms of audio against an 8-16 ms refresh, + // so this is the common case: hold what is drawn rather than redrawing identical bins. + smoothed.value = trail.nextOrNull(frameTimeNanos) ?: continue } } From 5b8e6e5b0a55c0c97f9f3b72f7edb2fe7ec918a8 Mon Sep 17 00:00:00 2001 From: davotoula <david.kaspar@gmail.com> Date: Sun, 27 Sep 2026 08:49:09 +0200 Subject: [PATCH 44/60] refactor(audio): simplify the visualizer pacing and stop it polling while idle Cleanup pass over the visualizer fix: - Fold SpectrumPacer into SpectrumTrail. It had one caller and only wrapped an ArrayDeque; its unused 64-frame default contradicted the real 24-frame cap. Its three tests were already covered by SpectrumTrailTest. - Size the PcmTapRegistry flow buffer from SpectrumTrail.MAX_BACKLOG_FRAMES instead of a separate 63. Both drop the stalest frame, so the visualizer keeps the same newest frames either way; the trail's cap is the one that actually bounds how far the picture can lag the sound. - Move the cluster-delivery test into SpectrumAudioBufferSinkTest on its existing monoPcm/sineShorts helpers instead of a second PCM encoder. Re-checked against the old 2-slot buffer: it still fails with 2 of 8. - Park the pacing loop off the frame clock while nothing is queued. It woke the main thread every vsync (up to 120 Hz) even when paused, which was new work for the non-animated bars and radial styles. It parks only after a nextOrNull has seen the empty queue, so the starvation reset still runs and the next cluster is not dumped at once to catch up. - Keep the cluster-timing explanation in the SpectrumTrail KDoc only. It had been restated in six places and the copies had already drifted (210 vs 220 ms). Also corrects the SpectrumCanvas KDoc's redraw-rate claim, which only held for bars and radial. --- .../playback/playerPool/PcmTapRegistry.kt | 19 ++-- .../playerPool/SpectrumAudioBufferSinkTest.kt | 38 ++++++++ .../playerPool/SpectrumBurstDeliveryTest.kt | 86 ------------------- .../amethyst/commons/audio/SpectrumPacer.kt | 57 ------------ .../amethyst/commons/audio/SpectrumTrail.kt | 42 +++++---- .../commons/audio/SpectrumPacerTest.kt | 67 --------------- .../commons/audio/SpectrumTrailTest.kt | 27 ++++-- .../amethyst/commons/audio/SpectrumCanvas.kt | 31 ++++--- 8 files changed, 105 insertions(+), 262 deletions(-) delete mode 100644 amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/playerPool/SpectrumBurstDeliveryTest.kt delete mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumPacer.kt delete mode 100644 commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumPacerTest.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/PcmTapRegistry.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/PcmTapRegistry.kt index 70b32c1a03..2fd9ce4a6d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/PcmTapRegistry.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/PcmTapRegistry.kt @@ -27,6 +27,7 @@ import androidx.media3.exoplayer.audio.TeeAudioProcessor import com.vitorpamplona.amethyst.commons.audio.AudioWindow import com.vitorpamplona.amethyst.commons.audio.Fft import com.vitorpamplona.amethyst.commons.audio.Spectrum +import com.vitorpamplona.amethyst.commons.audio.SpectrumTrail import com.vitorpamplona.amethyst.commons.audio.normalizeToPeakInPlace import com.vitorpamplona.amethyst.commons.audio.toLogBins import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -118,11 +119,6 @@ class SpectrumAudioBufferSink( object PcmTapRegistry { private const val MAX_TRACKED_FLOWS = 64 - // Frames buffered per media flow beyond the 1-frame replay. A cluster is ~15 fft frames (~0.33 s - // of audio, measured on a Pixel 9a); 63 leaves room for an unusually large one without letting a - // stalled UI bank more than ~1.5 s of stale spectrum. - private const val SPECTRUM_BUFFER_FRAMES = 63 - private val lock = Any() // access-order LinkedHashMap → eldest (least-recently-used) entries iterate first for eviction. @@ -178,16 +174,13 @@ object PcmTapRegistry { if (!fedByLiveSink && !stillCollected) iter.remove() } } - // The audio thread emits in clusters — the pipeline fills its output buffer ~3 times a - // second, so ~15 fft frames land within a few ms of each other (one per decoder call, - // or several from one large call) — while the UI collector sits on the main dispatcher - // and cannot run in between. A 2-slot buffer therefore kept only ~2 frames of each - // cluster and dropped the rest. Hold a whole cluster instead, and drop the STALEST - // frame rather than the newest if the UI does fall behind. (Spreading the cluster - // back over time is SpectrumTrail's job, not this flow's.) + // Frames arrive in clusters (see SpectrumTrail) emitted within a few ms on the audio + // thread, while the collector sits on the main dispatcher and cannot run in between; + // a 2-slot buffer kept ~2 frames of each cluster and dropped the rest. Hold as much as + // the visualizer's own backlog, which is the real lag bound, dropping the stalest. MutableSharedFlow( replay = 1, - extraBufferCapacity = SPECTRUM_BUFFER_FRAMES, + extraBufferCapacity = SpectrumTrail.MAX_BACKLOG_FRAMES, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) } diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/playerPool/SpectrumAudioBufferSinkTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/playerPool/SpectrumAudioBufferSinkTest.kt index 012ddf6e72..7cf470a967 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/playerPool/SpectrumAudioBufferSinkTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/playerPool/SpectrumAudioBufferSinkTest.kt @@ -24,7 +24,11 @@ import androidx.annotation.OptIn import androidx.media3.common.C import androidx.media3.common.util.UnstableApi import com.vitorpamplona.amethyst.commons.audio.Spectrum +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test @@ -33,6 +37,7 @@ import java.nio.ByteOrder import kotlin.math.PI import kotlin.math.sin +@kotlin.OptIn(ExperimentalCoroutinesApi::class) @OptIn(UnstableApi::class) class SpectrumAudioBufferSinkTest { private val fftSize = 64 @@ -161,4 +166,37 @@ class SpectrumAudioBufferSinkTest { assertEquals(fftSize * 1_000_000_000L / 44100, out.replayCache.last().durationNanos) } + + /** + * The audio thread emits a cluster of fft frames within a few ms, with no suspension point in + * between, while the UI collector sits on the main dispatcher and cannot interleave. Anything the + * registry's flow cannot hold at that instant is dropped; a 2-slot buffer kept 2 of every cluster. + */ + private fun deliveredFromOneCluster(framesInCluster: Int): Int { + var count = -1 + runTest { + val mediaId = "test://cluster-$framesInCluster" + val sink = SpectrumAudioBufferSink(fftSize = fftSize, binCount = binCount) + PcmTapRegistry.bind(mediaId, sink) + sink.flush(48000, 1, C.ENCODING_PCM_16BIT) + + val received = mutableListOf<Spectrum>() + val collector = launch { PcmTapRegistry.spectrumFor(mediaId).collect { received.add(it) } } + advanceUntilIdle() // let the collector subscribe + + sink.handleBuffer(monoPcm(sineShorts(k = 2, n = fftSize * framesInCluster))) + + advanceUntilIdle() // only now does the UI collector get to run + collector.cancel() + PcmTapRegistry.bind(null, sink) + count = received.size + } + return count + } + + @Test + fun everyFrameOfAClusterReachesTheVisualizer() { + assertEquals("cluster of 8", 8, deliveredFromOneCluster(8)) + assertEquals("cluster of 20", 20, deliveredFromOneCluster(20)) + } } diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/playerPool/SpectrumBurstDeliveryTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/playerPool/SpectrumBurstDeliveryTest.kt deleted file mode 100644 index d9c9a48c63..0000000000 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/service/playback/playerPool/SpectrumBurstDeliveryTest.kt +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.service.playback.playerPool - -import androidx.annotation.OptIn -import androidx.media3.common.C -import androidx.media3.common.util.UnstableApi -import com.vitorpamplona.amethyst.commons.audio.Spectrum -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertEquals -import org.junit.Test -import java.nio.ByteBuffer -import java.nio.ByteOrder - -/** - * The audio thread runs [SpectrumAudioBufferSink.handleBuffer] to completion, emitting EVERY fft - * frame of one decoder buffer synchronously with no suspension point in between. The UI collector - * lives on the main dispatcher and cannot interleave with that burst, so anything that does not fit - * in the flow's buffer at that instant is silently dropped by `tryEmit`. - * - * With a 2-slot buffer that capped the visualizer at two frames per decoder buffer no matter how - * much audio it carried, decoupling the update rate from the ~43 Hz the fft actually produces. - */ -@kotlin.OptIn(ExperimentalCoroutinesApi::class) -@OptIn(UnstableApi::class) -class SpectrumBurstDeliveryTest { - private val fftSize = 64 - - private fun monoPcm(totalSamples: Int): ByteBuffer { - val bb = ByteBuffer.allocate(totalSamples * 2).order(ByteOrder.LITTLE_ENDIAN) - for (i in 0 until totalSamples) bb.putShort(((i % 100) * 300).toShort()) - bb.flip() - return bb - } - - /** Frames the UI receives from ONE decoder buffer holding [framesInBurst] fft frames. */ - private fun deliveredFromOneBurst(framesInBurst: Int): Int { - var count = -1 - runTest { - val mediaId = "test://burst-$framesInBurst" - val sink = SpectrumAudioBufferSink(fftSize = fftSize, binCount = 16) - PcmTapRegistry.bind(mediaId, sink) - sink.flush(48000, 1, C.ENCODING_PCM_16BIT) - - val received = mutableListOf<Spectrum>() - val collector = launch { PcmTapRegistry.spectrumFor(mediaId).collect { received.add(it) } } - advanceUntilIdle() // let the collector subscribe - - sink.handleBuffer(monoPcm(fftSize * framesInBurst)) - - advanceUntilIdle() // now let the UI collector run - collector.cancel() - PcmTapRegistry.bind(null, sink) - count = received.size - } - return count - } - - @Test - fun everyFrameOfADecoderBurstReachesTheVisualizer() { - assertEquals("burst of 8", 8, deliveredFromOneBurst(8)) - assertEquals("burst of 20", 20, deliveredFromOneBurst(20)) - assertEquals("burst of 50", 50, deliveredFromOneBurst(50)) - } -} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumPacer.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumPacer.kt deleted file mode 100644 index e6f8cc2e1d..0000000000 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumPacer.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.commons.audio - -/** - * The bounded queue between the audio tap and the visualizer. - * - * Frames reach the UI in clusters, because the audio pipeline fills its output buffer in chunks - * (~15 frames about three times a second, measured on a Pixel 9a). This holds them until - * [SpectrumTrail] releases each one when its audio time comes due. It is bounded and evicts the - * stalest frame, so faster-than-real-time playback or a stalled UI cannot bank an ever-growing - * backlog and leave the picture permanently behind the sound. - * - * Not thread-safe by design: both ends run on the UI dispatcher. - */ -class SpectrumPacer( - private val capacity: Int = DEFAULT_CAPACITY, -) { - private val queue = ArrayDeque<Spectrum>(capacity) - - /** Queues a freshly decoded frame, evicting the stalest if the UI has fallen behind. */ - fun offer(frame: Spectrum) { - // Bound the queue so a stalled UI cannot bank an ever-growing backlog of stale spectrum and - // drift permanently behind the audio. The newest frame is always the one worth keeping. - while (queue.size >= capacity) queue.removeFirst() - queue.addLast(frame) - } - - fun isEmpty(): Boolean = queue.isEmpty() - - /** The next frame to draw, or null when the queue is empty and the last frame should persist. */ - fun next(): Spectrum? = queue.removeFirstOrNull() - - companion object { - // ~1.5 s of 1024-sample hops at 44.1 kHz: room for any realistic decoder buffer without - // letting a stalled UI accumulate an unbounded backlog. - const val DEFAULT_CAPACITY = 64 - } -} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrail.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrail.kt index 98e40f7770..08f2afd6be 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrail.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrail.kt @@ -21,16 +21,20 @@ package com.vitorpamplona.amethyst.commons.audio /** - * The whole per-displayed-frame step of the spectrum visualizer: release queued frames as their - * audio time comes due, apply the decay trail against what is already on screen, and return a + * The per-displayed-frame step of the spectrum visualizer: queue incoming frames, release each as + * its audio time comes due, apply the decay trail against what is already on screen, and return a * fresh array to draw. * - * Frames arrive in clusters (see [SpectrumPacer]). Releasing one per display refresh drained a - * ~15-frame cluster in ~110 ms at 120 Hz and then froze until the next one — measured as ~3 stalls - * of ~210 ms every second. Each frame instead stays up for the [Spectrum.durationNanos] of audio it - * describes, so a cluster spreads across the gap to the next. Time lost while starved is not owed - * back: when frames return after a gap they resume from the current frame time rather than being - * dumped at once to catch up. + * Why pacing is needed at all: the audio pipeline fills its output buffer in chunks, so frames reach + * the UI in clusters — ~15 frames roughly every 330 ms, measured on a Pixel 9a. Drawn on arrival a + * cluster collapses into one frame; released one per display refresh it plays out in ~110 ms at + * 120 Hz and then freezes (161 stalls averaging 211 ms over 52.8 s). Each frame instead stays up for + * the [Spectrum.durationNanos] of audio it describes, so a cluster spreads across the gap to the + * next. Time lost while starved is not owed back: after a gap, frames resume from the current frame + * time rather than being dumped at once to catch up. + * + * The backlog is capped at [capacity], evicting the stalest frame, so faster-than-real-time playback + * or a stalled UI cannot leave the picture drifting ever further behind the sound. * * The decay gives each bin an instant attack and a gradual release, so bars snap up to a transient * and fall back smoothly instead of flickering. @@ -42,17 +46,23 @@ package com.vitorpamplona.amethyst.commons.audio */ class SpectrumTrail( private val decay: Float, - capacity: Int = MAX_BACKLOG_FRAMES, + private val capacity: Int = MAX_BACKLOG_FRAMES, ) { - private val pacer = SpectrumPacer(capacity) + private val queue = ArrayDeque<Spectrum>(capacity) private var drawn = FloatArray(0) // When the frame at the head of the queue may be drawn, in the caller's frame-clock nanos. private var dueNanos = Long.MIN_VALUE private var starved = true - /** Queues a freshly decoded frame, evicting the stalest if the backlog is at capacity. */ - fun offer(frame: Spectrum) = pacer.offer(frame) + /** Queues a freshly decoded frame, evicting the stalest if the backlog is at [capacity]. */ + fun offer(frame: Spectrum) { + while (queue.size >= capacity) queue.removeFirst() + queue.addLast(frame) + } + + /** True when nothing is queued, so the caller can stop polling until the next [offer]. */ + fun isEmpty(): Boolean = queue.isEmpty() /** * The array to draw at [frameTimeNanos], or null when nothing new is due and the current one @@ -62,7 +72,7 @@ class SpectrumTrail( * instance is what signals Compose to redraw. Do NOT switch to in-place mutation. */ fun nextOrNull(frameTimeNanos: Long): FloatArray? { - if (pacer.isEmpty()) { + if (queue.isEmpty()) { starved = true return null } @@ -74,11 +84,10 @@ class SpectrumTrail( var result: FloatArray? = null while (dueNanos <= frameTimeNanos) { - val frame = pacer.next() ?: break + val frame = queue.removeFirstOrNull() ?: break result = decayedFrom(frame) dueNanos += frame.durationNanos } - return result } @@ -95,8 +104,7 @@ class SpectrumTrail( companion object { // A cluster is ~15 frames, so 24 (~0.5 s of audio) never trims ordinary playback, while - // capping how far the picture can lag the sound when frames come faster than real time - // (2x playback speed). + // bounding how far the picture can lag the sound (e.g. at 2x playback speed). const val MAX_BACKLOG_FRAMES = 24 } } diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumPacerTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumPacerTest.kt deleted file mode 100644 index 5dbaf18950..0000000000 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumPacerTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.commons.audio - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull - -class SpectrumPacerTest { - private fun frame(id: Float) = Spectrum(floatArrayOf(id)) - - private fun idOf(spectrum: Spectrum?) = spectrum?.bins?.first() - - @Test - fun drainsOneFramePerTickInArrivalOrder() { - val pacer = SpectrumPacer(capacity = 8) - pacer.offer(frame(1f)) - pacer.offer(frame(2f)) - pacer.offer(frame(3f)) - - assertEquals(1f, idOf(pacer.next())) - assertEquals(2f, idOf(pacer.next())) - assertEquals(3f, idOf(pacer.next())) - } - - @Test - fun returnsNullWhenStarvedSoTheCallerCanHoldTheLastFrame() { - val pacer = SpectrumPacer(capacity = 8) - assertNull(pacer.next()) - - pacer.offer(frame(1f)) - assertEquals(1f, idOf(pacer.next())) - assertNull(pacer.next()) - } - - @Test - fun overCapacityDropsTheStalestFrameNotTheNewest() { - val pacer = SpectrumPacer(capacity = 3) - pacer.offer(frame(1f)) - pacer.offer(frame(2f)) - pacer.offer(frame(3f)) - pacer.offer(frame(4f)) // evicts 1 - - assertEquals(2f, idOf(pacer.next())) - assertEquals(3f, idOf(pacer.next())) - assertEquals(4f, idOf(pacer.next())) - assertNull(pacer.next()) - } -} diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrailTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrailTest.kt index 39a73cb261..8a6ffab71a 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrailTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumTrailTest.kt @@ -22,18 +22,12 @@ package com.vitorpamplona.amethyst.commons.audio import kotlin.test.Test import kotlin.test.assertContentEquals +import kotlin.test.assertFalse import kotlin.test.assertNotSame import kotlin.test.assertNull +import kotlin.test.assertTrue -/** - * The per-displayed-frame step of the visualizer: release queued frames as their audio time comes - * due, apply the decay trail against what is drawn, and hand back a fresh array. - * - * Frames reach the UI in clusters (~15 at a time, ~3 times a second, measured on a Pixel 9a) because - * the audio pipeline fills its output buffer in chunks. Releasing one per display refresh drained a - * cluster in ~110 ms at 120 Hz and then froze for ~220 ms. Frames must instead be released at the - * rate of the audio they describe, so a cluster spreads across the gap to the next one. - */ +/** Pacing, starvation, backlog and decay rules of [SpectrumTrail]; see its KDoc for why each exists. */ class SpectrumTrailTest { private val ms = 1_000_000L @@ -171,4 +165,19 @@ class SpectrumTrailTest { // mutableStateOf compares by reference; reusing one array would never trigger a redraw. assertNotSame(first, second) } + + @Test + fun reportsEmptyOnlyOnceEveryQueuedFrameIsReleasedSoTheCallerKnowsWhenToPark() { + val trail = SpectrumTrail(decay = 0f) + assertTrue(trail.isEmpty()) + + trail.offer(frame(1f)) + trail.offer(frame(2f)) + assertFalse(trail.isEmpty()) + + trail.nextOrNull(0) + assertFalse(trail.isEmpty()) // frame 2 is queued but not yet due + trail.nextOrNull(20 * ms) + assertTrue(trail.isEmpty()) + } } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumCanvas.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumCanvas.kt index 56a2ec66d9..a89eade52f 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumCanvas.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/SpectrumCanvas.kt @@ -29,6 +29,7 @@ import androidx.compose.runtime.withFrameMillis import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.drawscope.DrawScope +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow /** @@ -36,10 +37,9 @@ import kotlinx.coroutines.flow.Flow * then calls [draw] inside the Canvas draw lambda. The fast-changing state is read * ONLY in the draw lambda, so new frames trigger the draw phase, never recomposition. * - * Frames are released through [SpectrumTrail] as the audio they describe comes due — they arrive - * in clusters, and neither dumping a cluster into state nor draining it one per vsync looks live. - * The pacing loop runs every frame but only writes state when a frame is due, so the redraw rate - * tracks the ~43-47 Hz the fft produces rather than the display. + * Frames are released through [SpectrumTrail] as the audio they describe comes due. The pacing loop + * only writes state when a frame is due, and parks entirely while nothing is queued (paused, idle), + * so on its own it redraws at the ~43-47 Hz the fft produces and costs nothing when silent. * * Pass [animated] = false for non-time-varying styles (bars, radial): that drops the monotonic clock, * whose whole purpose is to redraw every frame even when the spectrum has not moved. @@ -55,21 +55,26 @@ fun SpectrumCanvas( ) { val smoothed = remember { mutableStateOf(FloatArray(0)) } - // Frames arrive in clusters, so they are queued and released as their audio time comes due. - // Writing a cluster straight into `smoothed` collapses it into one draw; releasing one per vsync - // races through it and then freezes. Pacing and decay live in SpectrumTrail so they are testable - // without a Compose harness. val trail = remember(spectrum, decay) { SpectrumTrail(decay) } + val arrivals = remember(trail) { Channel<Unit>(Channel.CONFLATED) } LaunchedEffect(spectrum, trail) { - spectrum.collect { trail.offer(it) } + spectrum.collect { + trail.offer(it) + arrivals.trySend(Unit) + } } LaunchedEffect(trail) { while (true) { - val frameTimeNanos = withFrameNanos { it } - // Null means nothing new is due. Frames cover ~21 ms of audio against an 8-16 ms refresh, - // so this is the common case: hold what is drawn rather than redrawing identical bins. - smoothed.value = trail.nextOrNull(frameTimeNanos) ?: continue + val next = trail.nextOrNull(withFrameNanos { it }) + if (next != null) { + smoothed.value = next + } else if (trail.isEmpty()) { + // Park off the frame clock until the next offer. This must follow a nextOrNull that + // saw the empty queue: that call records the starvation which stops the next cluster + // from being dumped at once to "catch up". + arrivals.receive() + } } } From 9af317786fda45fc29d7cb5c62a525437de3e37c Mon Sep 17 00:00:00 2001 From: davotoula <david.kaspar@gmail.com> Date: Sun, 27 Sep 2026 10:13:16 +0200 Subject: [PATCH 45/60] fix(strings): turn count-bearing strings into plurals Fifteen commonsUI strings put a count next to a noun or verb that agrees with it ("%1$d groups moved", "%1$d caches here", "Search %1$s people"). As a <string> they only have one form, which Czech, Polish and similar languages cannot express. cyberspace_bag_dropped worked around it with a hand-rolled _many sibling chosen by `== 1`, which is wrong for any language whose `one` category covers more than 1. Convert them to <plurals> and resolve them with the plural helpers. The existing hu, hi and pl translations are carried across with their full category sets (pl gets one/few/many/other). geocache_map_pin_count had no call site and is removed. --- .../amethyst/ui/note/types/CyberspaceBag.kt | 17 ++-- .../backups/BackupConflictEventViews.kt | 3 +- .../backups/BackupConflictListViews.kt | 2 +- .../cordnGroup/CordnCreateGroupScreen.kt | 6 +- .../loggedIn/geocaches/GeocacheHuntRow.kt | 3 +- .../detail/GeocacheAddToHuntSheet.kt | 3 +- .../loggedIn/geohash/GeocachesHereChip.kt | 4 +- .../qrcode/scanner/QrScannerOverlay.kt | 3 +- .../settings/cordn/CordnCoordinatorsScreen.kt | 5 +- .../settings/cordn/CordnMigrateScreen.kt | 3 +- .../values-hi-rIN/strings.xml | 47 ++++++++--- .../values-hu-rHU/strings.xml | 47 ++++++++--- .../values-pl-rPL/strings.xml | 65 +++++++++++++--- .../composeResources/values/strings.xml | 77 +++++++++++++++---- .../commons/cordn/ui/CoordinatorHealthRow.kt | 3 +- 15 files changed, 213 insertions(+), 75 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CyberspaceBag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CyberspaceBag.kt index 6249d38fed..1490ae1cde 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CyberspaceBag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CyberspaceBag.kt @@ -50,7 +50,6 @@ import com.vitorpamplona.amethyst.commons.resources.cyberspace_bag_cancel import com.vitorpamplona.amethyst.commons.resources.cyberspace_bag_damaged import com.vitorpamplona.amethyst.commons.resources.cyberspace_bag_destination import com.vitorpamplona.amethyst.commons.resources.cyberspace_bag_dropped -import com.vitorpamplona.amethyst.commons.resources.cyberspace_bag_dropped_many import com.vitorpamplona.amethyst.commons.resources.cyberspace_bag_empty import com.vitorpamplona.amethyst.commons.resources.cyberspace_bag_measuring import com.vitorpamplona.amethyst.commons.resources.cyberspace_bag_no_hint @@ -80,6 +79,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource /** @@ -242,7 +242,7 @@ private fun Contents( null -> Footnote(stringResource(Res.string.cyberspace_bag_damaged)) is CyberspaceBagContents.Opaque -> - Footnote(asText(contents.bytes) ?: stringResource(Res.string.cyberspace_bag_opaque, contents.bytes.size)) + Footnote(asText(contents.bytes) ?: pluralStringResource(Res.plurals.cyberspace_bag_opaque, contents.bytes.size, contents.bytes.size)) is CyberspaceBagContents.Items -> { if (contents.items.isEmpty() && contents.dropped == 0) { @@ -256,13 +256,7 @@ private fun Contents( // still worth saying that something was thrown away. if (contents.dropped > 0) { Spacer(Modifier.height(6.dp)) - Footnote( - if (contents.dropped == 1) { - stringResource(Res.string.cyberspace_bag_dropped, contents.dropped) - } else { - stringResource(Res.string.cyberspace_bag_dropped_many, contents.dropped) - }, - ) + Footnote(pluralStringResource(Res.plurals.cyberspace_bag_dropped, contents.dropped, contents.dropped)) } } } @@ -322,9 +316,10 @@ private fun Footnote(text: String) { private fun outOfReach(estimateMillis: Long): String { val minutes = estimateMillis / 60_000L return if (minutes >= MINUTES_PER_HOUR) { - stringResource(Res.string.cyberspace_bag_too_slow_hours, (minutes / MINUTES_PER_HOUR).toInt()) + val hours = (minutes / MINUTES_PER_HOUR).toInt() + pluralStringResource(Res.plurals.cyberspace_bag_too_slow_hours, hours, hours) } else { - stringResource(Res.string.cyberspace_bag_too_slow_minutes, minutes.toInt()) + pluralStringResource(Res.plurals.cyberspace_bag_too_slow_minutes, minutes.toInt(), minutes.toInt()) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictEventViews.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictEventViews.kt index 1916f09286..b348bb27a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictEventViews.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictEventViews.kt @@ -144,6 +144,7 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.elements.BannerImage +import com.vitorpamplona.amethyst.ui.pluralStringRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.LoadRelayGroupChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser @@ -420,7 +421,7 @@ private fun PeopleSearch( singleLine = true, shape = RoundedCornerShape(14.dp), leadingIcon = { Icon(symbol = MaterialSymbols.Search, contentDescription = null) }, - placeholder = { Text(stringRes(Res.string.backup_review_search_people, count.toString())) }, + placeholder = { Text(pluralStringRes(Res.plurals.backup_review_search_people, count, count.toString())) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt index 19d694dd09..9be726a715 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/backups/BackupConflictListViews.kt @@ -754,7 +754,7 @@ private fun LazyListScope.trustProviderItems( Icon(symbol = MaterialSymbols.Group, contentDescription = null, tint = tones.changed) } Column { - Text(stringRes(Res.string.backup_review_services_changed, affected.toString(), services.size.toString()), fontSize = 24.sp, lineHeight = 28.sp, fontWeight = FontWeight.Bold) + Text(pluralStringResource(Res.plurals.backup_review_services_changed, services.size, affected.toString(), services.size.toString()), fontSize = 24.sp, lineHeight = 28.sp, fontWeight = FontWeight.Bold) Text(stringRes(Res.string.backup_review_services_changed_explainer), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.placeholderText) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnCreateGroupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnCreateGroupScreen.kt index 6479e51eb5..d1d0f28e89 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnCreateGroupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/cordnGroup/CordnCreateGroupScreen.kt @@ -387,7 +387,7 @@ fun CordnCreateGroupScreen( if (draft.showStale) { stringRes(Res.string.cordn_coordinators_hide_older) } else { - stringRes(Res.string.cordn_coordinators_show_older, stale.size) + pluralStringRes(Res.plurals.cordn_coordinators_show_older, stale.size, stale.size) }, ) } @@ -462,7 +462,7 @@ fun CordnCreateGroupScreen( // different answers and only one of them is final. // Reporting the first for the second sends someone // off to paste a pubkey by hand over a timeout. - stringRes(Res.string.cordn_coordinators_discover_unheard, result.unreachable.size) + pluralStringRes(Res.plurals.cordn_coordinators_discover_unheard, result.unreachable.size, result.unreachable.size) }, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -927,7 +927,7 @@ private fun CoordinatorSummary( text = when { coverage == null || !coverage.answered -> stringRes(Res.string.cordn_create_coverage_unknown) - reached == rosterSize -> stringRes(Res.string.cordn_create_coverage_all, rosterSize) + reached == rosterSize -> pluralStringRes(Res.plurals.cordn_create_coverage_all, rosterSize, rosterSize) else -> stringRes(Res.string.cordn_create_coverage_partial, reached ?: 0, rosterSize) }, style = MaterialTheme.typography.labelSmall, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/GeocacheHuntRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/GeocacheHuntRow.kt index 0f1ff285f3..6bb8654ded 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/GeocacheHuntRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/GeocacheHuntRow.kt @@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.commons.resources.geocache_hunt_caches import com.vitorpamplona.amethyst.commons.resources.geocache_hunt_progress import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.rememberGeocachePalette +import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -119,7 +120,7 @@ fun GeocacheHuntRow( Text( text = stringRes(Res.string.geocache_hunt_progress, doneCount, caches.size) + - " · " + stringRes(Res.string.geocache_hunt_caches, caches.size), + " · " + pluralStringRes(Res.plurals.geocache_hunt_caches, caches.size, caches.size), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.kt index fa89098694..48fb28b80e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.kt @@ -56,6 +56,7 @@ import com.vitorpamplona.amethyst.commons.resources.geocache_hunt_already_on import com.vitorpamplona.amethyst.commons.resources.geocache_hunt_caches import com.vitorpamplona.amethyst.commons.resources.geocache_hunt_new import com.vitorpamplona.amethyst.commons.resources.geocache_hunt_none_yet +import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Address @@ -178,7 +179,7 @@ fun GeocacheAddToHuntSheet( if (already) { stringRes(Res.string.geocache_hunt_already_on) } else { - stringRes(Res.string.geocache_hunt_caches, hunt.geocaches().size) + hunt.geocaches().size.let { pluralStringRes(Res.plurals.geocache_hunt_caches, it, it) } }, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeocachesHereChip.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeocachesHereChip.kt index 744cc2c59d..a279748574 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeocachesHereChip.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeocachesHereChip.kt @@ -48,7 +48,7 @@ import com.vitorpamplona.amethyst.commons.resources.geocache_caches_here import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.note.rememberGeocachePalette -import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.dal.GeocachesHereFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.datasource.GeocachesFilterAssemblerSubscription @@ -121,7 +121,7 @@ private fun GeocachesHereCount( tint = palette.live, ) Text( - text = stringRes(Res.string.geocache_caches_here, count), + text = pluralStringRes(Res.plurals.geocache_caches_here, count, count), style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerOverlay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerOverlay.kt index 5de6999b6b..3a04b0a27e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerOverlay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrScannerOverlay.kt @@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.commons.resources.qr_scanner_sequence_progress import com.vitorpamplona.amethyst.commons.resources.qr_scanner_torch_off import com.vitorpamplona.amethyst.commons.resources.qr_scanner_torch_on import com.vitorpamplona.amethyst.commons.resources.qr_scanner_zoom_reset +import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.stringRes import kotlin.math.max @@ -255,7 +256,7 @@ private fun HintStack( val message = when { state.notice != null -> state.notice - progress != null -> stringRes(Res.string.qr_scanner_sequence_progress, progress.first, progress.second) + progress != null -> pluralStringRes(Res.plurals.qr_scanner_sequence_progress, progress.second, progress.first, progress.second) state.candidates.size > 1 -> stringRes(Res.string.qr_scanner_pick_one) state.isDark && !state.torchOn -> stringRes(Res.string.qr_scanner_dark_hint) else -> null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt index cdae46aa2a..d59da9ba7e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnCoordinatorsScreen.kt @@ -114,6 +114,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.model.cordn.CordnRuntime import com.vitorpamplona.amethyst.ui.note.UserPicture +import com.vitorpamplona.amethyst.ui.pluralStringRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CopyableKeyRow import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSection @@ -444,7 +445,7 @@ private fun HealthLine(state: CoordinatorHealth.State) { val text = when { state.isUnknown -> stringRes(Res.string.cordn_coordinators_health_unknown) - state.isDown -> stringRes(Res.string.cordn_coordinators_health_down, state.consecutiveFailures) + state.isDown -> pluralStringRes(Res.plurals.cordn_coordinators_health_down, state.consecutiveFailures, state.consecutiveFailures) state.consecutiveFailures > 0 -> stringRes(Res.string.cordn_coordinators_health_retrying) else -> stringRes(Res.string.cordn_coordinators_health_ok) } @@ -535,7 +536,7 @@ private fun DiscoverCoordinators( } else { // "Nobody is announcing" and "we were not told" are // different answers and only one of them is final. - stringRes(Res.string.cordn_coordinators_discover_unheard, found.unreachable.size) + pluralStringRes(Res.plurals.cordn_coordinators_discover_unheard, found.unreachable.size, found.unreachable.size) }, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnMigrateScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnMigrateScreen.kt index 6c55c2bbb0..299e0c9c97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnMigrateScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/cordn/CordnMigrateScreen.kt @@ -80,6 +80,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.model.cordn.AndroidCordnBlobStore import com.vitorpamplona.amethyst.model.cordn.CordnRuntime +import com.vitorpamplona.amethyst.ui.pluralStringRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner @@ -351,7 +352,7 @@ private fun ReceiveSide( modifier = Modifier.fillMaxWidth(), ) - done?.let { Text(stringRes(Res.string.cordn_migrate_done, it), style = MaterialTheme.typography.bodyMedium) } + done?.let { Text(pluralStringRes(Res.plurals.cordn_migrate_done, it, it), style = MaterialTheme.typography.bodyMedium) } error?.let { Text(it, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.error) } Button( diff --git a/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml b/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml index ab38725872..d4f890da43 100644 --- a/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml @@ -206,7 +206,10 @@ <string name="geocache_hunt_new">नई खोज…</string> <string name="geocache_hunt_none_yet">आपने कोई खोज नहीं की अब तक।</string> <string name="geocache_hunt_already_on">पहले से इस खोज में लगे</string> - <string name="geocache_caches_here">%1$d कोष यहाँ</string> + <plurals name="geocache_caches_here"> + <item quantity="one">%1$d कोष यहाँ</item> + <item quantity="other">%1$d कोष यहाँ</item> + </plurals> <string name="geocache_photo_remove">इस चित्र को हटाएँ</string> <string name="geocache_archived_notice">इस कोष को संगृहीत कर दिया गया। यह अब खेल में नहीं है।</string> <string name="geocache_ftf_won_by">सर्वप्रथम खोज का कथन %1$s द्वारा</string> @@ -265,7 +268,6 @@ <string name="geocache_map_recenter">मुझ पर मध्यीकरण</string> <string name="geocache_map_long_press">मानचित्र पर दीर्घ दबाएँ वहाँ एक कोष छिपाने के लिए</string> <string name="geocache_map_open">खोलें</string> - <string name="geocache_map_pin_count">%1$d कोष</string> <string name="geocache_new_section_where">कहाँ</string> <string name="geocache_new_section_what">क्या</string> <string name="geocache_new_section_extras">अतिरिक्त</string> @@ -293,7 +295,10 @@ <string name="geocache_new_needs_location">कोष कहाँ छिपाया जाए चयन करें।</string> <string name="geocache_hunt_progress">%2$d में से %1$d प्राप्त</string> <string name="geocache_hunt_start">खोज आरम्भ करें</string> - <string name="geocache_hunt_caches">%1$d कोष</string> + <plurals name="geocache_hunt_caches"> + <item quantity="one">%1$d कोष</item> + <item quantity="other">%1$d कोष</item> + </plurals> <string name="geocache_hunt_title">शीर्षक</string> <string name="geocache_hunt_description">विवरण</string> <string name="geocache_hunt_banner">ध्वज चित्र</string> @@ -342,7 +347,10 @@ <string name="qr_scanner_no_code_in_image">उस चित्र में से कोई क्यूआर॰ चित्र अक्षरराशि प्राप्त नहीं</string> <string name="qr_scanner_clipboard_empty">टाँकाफलक में कुछ भी नहीं परखने के लिए</string> <string name="qr_scanner_pick_one">अक्षरराशि का चयन करें</string> - <string name="qr_scanner_sequence_progress">%2$s में से %1$s भाग प्राप्त। आगे बढें</string> + <plurals name="qr_scanner_sequence_progress"> + <item quantity="one">%2$s में से %1$s भाग प्राप्त। आगे बढें</item> + <item quantity="other">%2$s में से %1$s भाग प्राप्त। आगे बढें</item> + </plurals> <string name="qr_scanner_zoom_reset">विस्तारण पुनःस्थापना</string> <string name="qr_scanner_unsupported_title">अमेथिस्ट इस अक्षरराशि को नहीं खोल सकता</string> <string name="qr_scanner_unsupported_secret">इस अक्षरराशि में एक निजी कुंचिका अथवा जोडनेवाला रहस्य है। इसलिए इसकी विषयवस्तु यहाँ दिखाया नहीं जाता। अनुकूल पटल का उपयोग करें।</string> @@ -5253,16 +5261,27 @@ <string name="cyberspace_bag_cancel">रोकें</string> <string name="cyberspace_bag_measuring">खोज का मूल्यांकन इस यन्त्र पर…</string> <string name="cyberspace_bag_progress">खोज सम्पन्न %2$s में से %1$s क्षेत्रों में</string> - <string name="cyberspace_bag_too_slow_minutes">अप्राप्य। लगभग %1$d मिनुट का खोज इस यन्त्र पर</string> - <string name="cyberspace_bag_too_slow_hours">अप्राप्य। लगभग %1$d घण्टों का खोज इस यन्त्र पर</string> + <plurals name="cyberspace_bag_too_slow_minutes"> + <item quantity="one">अप्राप्य। लगभग %1$d मिनुट का खोज इस यन्त्र पर</item> + <item quantity="other">अप्राप्य। लगभग %1$d मिनुट का खोज इस यन्त्र पर</item> + </plurals> + <plurals name="cyberspace_bag_too_slow_hours"> + <item quantity="one">अप्राप्य। लगभग %1$d घण्टों का खोज इस यन्त्र पर</item> + <item quantity="other">अप्राप्य। लगभग %1$d घण्टों का खोज इस यन्त्र पर</item> + </plurals> <string name="cyberspace_bag_not_found">सहायक संकेत में नामित पेटिका में नहीं</string> <string name="cyberspace_bag_damaged">क्षेत्र प्राप्त। पर विषयवस्तु पढने में असफल</string> - <string name="cyberspace_bag_dropped">%1$d वस्तु जो यहाँ छिपाया गया था उसके हस्ताक्षर से मेल नहीं तथा परित्यक्त</string> - <string name="cyberspace_bag_dropped_many">%1$d वस्तु जो यहाँ छिपाए गए थे उनके हस्ताक्षरों से मेल नहीं तथा परित्यक्त</string> + <plurals name="cyberspace_bag_dropped"> + <item quantity="one">%1$d वस्तु जो यहाँ छिपाया गया था उसके हस्ताक्षर से मेल नहीं तथा परित्यक्त</item> + <item quantity="other">%1$d वस्तु जो यहाँ छिपाए गए थे उनके हस्ताक्षरों से मेल नहीं तथा परित्यक्त</item> + </plurals> <string name="cyberspace_bag_empty">खोला। पर भीतर कुछ नहीं था</string> <string name="cyberspace_bag_unsigned">हस्ताक्षररहित। इसलिए यह कर्ता केवल एक कथन है</string> <string name="cyberspace_bag_unknown_kind">प्रकार %1$d का वस्तु। जिसे यह ग्राहक चित्रित नहीं करता</string> - <string name="cyberspace_bag_opaque">%1$d अष्टकों का अमुक जिसे यह ग्राहक पढता नहीं</string> + <plurals name="cyberspace_bag_opaque"> + <item quantity="one">%1$d अष्टकों का अमुक जिसे यह ग्राहक पढता नहीं</item> + <item quantity="other">%1$d अष्टकों का अमुक जिसे यह ग्राहक पढता नहीं</item> + </plurals> <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> <string name="backup_action_block_again">पुनःबाधित करें %1$s को</string> <string name="backup_action_keep_count">रखें %1$s</string> @@ -5501,13 +5520,19 @@ <string name="backup_review_renamed">पुनःनामकृत</string> <string name="backup_review_renamed_tag">पुनःनामकृत</string> <string name="backup_review_saved">इस यन्त्र पर</string> - <string name="backup_review_search_people">%1$s लोग खोजें</string> + <plurals name="backup_review_search_people"> + <item quantity="one">%1$s लोग खोजें</item> + <item quantity="other">%1$s लोग खोजें</item> + </plurals> <string name="backup_review_searching_tag">खोज मध्य</string> <string name="backup_review_service">सेवा</string> <string name="backup_review_service_followers">अनुचर</string> <string name="backup_review_service_rank">श्रेणी</string> <string name="backup_review_service_topics">विषयसूची</string> - <string name="backup_review_services_changed">%2$s में से %1$s सेवाएँ परिवर्तित</string> + <plurals name="backup_review_services_changed"> + <item quantity="one">%2$s में से %1$s सेवाएँ परिवर्तित</item> + <item quantity="other">%2$s में से %1$s सेवाएँ परिवर्तित</item> + </plurals> <string name="backup_review_services_changed_explainer">वे अब अन्य व्यक्ति से आते है। अथवा किसी से नहीं</string> <string name="backup_review_still_blocked">अभी भी बाधित</string> <string name="backup_review_storing_tag">अभिलेखन मध्य</string> diff --git a/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml b/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml index bc0ba670c6..04d81c7de6 100644 --- a/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml @@ -206,7 +206,10 @@ <string name="geocache_hunt_new">Új vadászat…</string> <string name="geocache_hunt_none_yet">Még nem hozott létre vadászatot.</string> <string name="geocache_hunt_already_on">Már szerepel ebben a vadászatban</string> - <string name="geocache_caches_here">%1$d láda van itt</string> + <plurals name="geocache_caches_here"> + <item quantity="one">%1$d láda van itt</item> + <item quantity="other">%1$d láda van itt</item> + </plurals> <string name="geocache_photo_remove">Fénykép eltávolítása</string> <string name="geocache_archived_notice">Ezt a ládát archiválták. Már nem játszik.</string> <string name="geocache_ftf_won_by">Az első megtalálást %1$s szerezte meg</string> @@ -265,7 +268,6 @@ <string name="geocache_map_recenter">Középre</string> <string name="geocache_map_long_press">Egy láda elrejtéséhez nyomja meg hossza a térképet</string> <string name="geocache_map_open">Megnyitás</string> - <string name="geocache_map_pin_count">%1$d láda</string> <string name="geocache_new_section_where">Hol</string> <string name="geocache_new_section_what">Mi</string> <string name="geocache_new_section_extras">Extrák</string> @@ -293,7 +295,10 @@ <string name="geocache_new_needs_location">Válassza ki, hogy hol van elrejtve a láda.</string> <string name="geocache_hunt_progress">%2$d / %1$d megtalálva</string> <string name="geocache_hunt_start">Vadászat indítása</string> - <string name="geocache_hunt_caches">%1$d láda</string> + <plurals name="geocache_hunt_caches"> + <item quantity="one">%1$d láda</item> + <item quantity="other">%1$d láda</item> + </plurals> <string name="geocache_hunt_title">Cím</string> <string name="geocache_hunt_description">Leírás</string> <string name="geocache_hunt_banner">Borítókép</string> @@ -342,7 +347,10 @@ <string name="qr_scanner_no_code_in_image">Nem található QR-kód a képen</string> <string name="qr_scanner_clipboard_empty">A vágólapon nincs semmi a beolvasáshoz</string> <string name="qr_scanner_pick_one">Érintse meg a beolvasandó kódot</string> - <string name="qr_scanner_sequence_progress">%1$s darabot sikerült beolvasni a(z) %2$s darab közül, folytatás</string> + <plurals name="qr_scanner_sequence_progress"> + <item quantity="one">%1$s darabot sikerült beolvasni a(z) %2$s darab közül, folytatás</item> + <item quantity="other">%1$s darabot sikerült beolvasni a(z) %2$s darab közül, folytatás</item> + </plurals> <string name="qr_scanner_zoom_reset">Nagyítás visszaállítása</string> <string name="qr_scanner_unsupported_title">Az Amethyst nem tudja megnyitni ezt a kódot</string> <string name="qr_scanner_unsupported_secret">Ez a kód egy titkos kulcsot vagy párosítási titkot tartalmaz, ezért a tartalma itt nem jelenik meg. Használja azt a képernyőt, ahol meg kell adni.</string> @@ -5253,16 +5261,27 @@ <string name="cyberspace_bag_cancel">Leállítás</string> <string name="cyberspace_bag_measuring">A keresés árazása ezen az eszközön…</string> <string name="cyberspace_bag_progress">%2$s régióból %1$s átkutatva</string> - <string name="cyberspace_bag_too_slow_minutes">Elérhetetlen: körülbelül %1$d perc keresés ezen az eszközön</string> - <string name="cyberspace_bag_too_slow_hours">Elérhetetlen: körülbelül %1$d óra keresés ezen az eszközön</string> + <plurals name="cyberspace_bag_too_slow_minutes"> + <item quantity="one">Elérhetetlen: körülbelül %1$d perc keresés ezen az eszközön</item> + <item quantity="other">Elérhetetlen: körülbelül %1$d perc keresés ezen az eszközön</item> + </plurals> + <plurals name="cyberspace_bag_too_slow_hours"> + <item quantity="one">Elérhetetlen: körülbelül %1$d óra keresés ezen az eszközön</item> + <item quantity="other">Elérhetetlen: körülbelül %1$d óra keresés ezen az eszközön</item> + </plurals> <string name="cyberspace_bag_not_found">Nincs abban a dobozban, amelyet a nyom megnevezett</string> <string name="cyberspace_bag_damaged">A régió megvan, de a tartalmát nem sikerült beolvasni</string> - <string name="cyberspace_bag_dropped">Az itt elrejtett %1$d elem nem egyezett az aláírásával, ezért el lett vetve</string> - <string name="cyberspace_bag_dropped_many">Az itt elrejtett %1$d elem nem egyezett az aláírásával, ezért el lettek vetve</string> + <plurals name="cyberspace_bag_dropped"> + <item quantity="one">Az itt elrejtett %1$d elem nem egyezett az aláírásával, ezért el lett vetve</item> + <item quantity="other">Az itt elrejtett %1$d elem nem egyezett az aláírásával, ezért el lettek vetve</item> + </plurals> <string name="cyberspace_bag_empty">Kinyitva, és nem volt benne semmi</string> <string name="cyberspace_bag_unsigned">Aláírás nélküli, így ez a szerző csak állítás</string> <string name="cyberspace_bag_unknown_kind">Egy %1$d típusú elem, amelyet ez a kliens nem jelenít meg</string> - <string name="cyberspace_bag_opaque">%1$d bájt valamiből, amit ez a kliens nem olvas</string> + <plurals name="cyberspace_bag_opaque"> + <item quantity="one">%1$d bájt valamiből, amit ez a kliens nem olvas</item> + <item quantity="other">%1$d bájt valamiből, amit ez a kliens nem olvas</item> + </plurals> <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> <string name="backup_action_block_again">%1$s újbóli letiltása</string> <string name="backup_action_keep_count">%1$s megtartása</string> @@ -5501,13 +5520,19 @@ <string name="backup_review_renamed">átnevezte</string> <string name="backup_review_renamed_tag">Átnevezve</string> <string name="backup_review_saved">Ezen az eszközön</string> - <string name="backup_review_search_people">%1$s személy keresése</string> + <plurals name="backup_review_search_people"> + <item quantity="one">%1$s személy keresése</item> + <item quantity="other">%1$s személy keresése</item> + </plurals> <string name="backup_review_searching_tag">Keresés</string> <string name="backup_review_service">Szolgáltatás</string> <string name="backup_review_service_followers">Követők</string> <string name="backup_review_service_rank">Rang</string> <string name="backup_review_service_topics">Témák</string> - <string name="backup_review_services_changed">%2$s szolgáltatásból %1$s módosult</string> + <plurals name="backup_review_services_changed"> + <item quantity="one">%2$s szolgáltatásból %1$s módosult</item> + <item quantity="other">%2$s szolgáltatásból %1$s módosult</item> + </plurals> <string name="backup_review_services_changed_explainer">Most már valaki mástól vagy senkitől sem származnak</string> <string name="backup_review_still_blocked">Továbbra is le van tiltva</string> <string name="backup_review_storing_tag">Tárolás</string> diff --git a/commonsUI/src/commonMain/composeResources/values-pl-rPL/strings.xml b/commonsUI/src/commonMain/composeResources/values-pl-rPL/strings.xml index ccd2e12d38..1166642559 100644 --- a/commonsUI/src/commonMain/composeResources/values-pl-rPL/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-pl-rPL/strings.xml @@ -214,7 +214,12 @@ <string name="geocache_hunt_new">Nowe polowanie…</string> <string name="geocache_hunt_none_yet">Nie dokonałeś jeszcze polowania.</string> <string name="geocache_hunt_already_on">Już podczas tego polowania</string> - <string name="geocache_caches_here">%1$d skrzynek tutaj</string> + <plurals name="geocache_caches_here"> + <item quantity="one">%1$d skrzynka tutaj</item> + <item quantity="few">%1$d skrzynki tutaj</item> + <item quantity="many">%1$d skrzynek tutaj</item> + <item quantity="other">%1$d skrzynki tutaj</item> + </plurals> <string name="geocache_photo_remove">Usuń to zdjęcie</string> <string name="geocache_archived_notice">Ta skrzynka została zarchiwizowana. Nie jest już w grze.</string> <string name="geocache_ftf_won_by">Pierwszy, który znalazł – zgłoszone przez %1$s</string> @@ -273,7 +278,6 @@ <string name="geocache_map_recenter">Wyśrodkuj na mnie</string> <string name="geocache_map_long_press">Naciśnij i przytrzymaj mapę, aby ukryć skrzynkę</string> <string name="geocache_map_open">Otwórz</string> - <string name="geocache_map_pin_count">%1$d skrzynek</string> <string name="geocache_new_section_where">Gdzie</string> <string name="geocache_new_section_what">Co</string> <string name="geocache_new_section_extras">Dodatki</string> @@ -301,7 +305,12 @@ <string name="geocache_new_needs_location">Wybierz gdzie skrzynka jest ukryta.</string> <string name="geocache_hunt_progress">Znaleziono %1$d z %2$d</string> <string name="geocache_hunt_start">Rozpocznij polowanie</string> - <string name="geocache_hunt_caches">%1$d skrzynek</string> + <plurals name="geocache_hunt_caches"> + <item quantity="one">%1$d skrzynka</item> + <item quantity="few">%1$d skrzynki</item> + <item quantity="many">%1$d skrzynek</item> + <item quantity="other">%1$d skrzynki</item> + </plurals> <string name="geocache_hunt_title">Tytuł</string> <string name="geocache_hunt_description">Opis</string> <string name="geocache_hunt_banner">Obraz banera</string> @@ -352,7 +361,12 @@ <string name="qr_scanner_no_code_in_image">Nie znaleziono kodu QR na tym obrazku</string> <string name="qr_scanner_clipboard_empty">Nic w schowku do skanowania</string> <string name="qr_scanner_pick_one">Wybierz żądany kod</string> - <string name="qr_scanner_sequence_progress">Zdobyto %1$s z %2$s części — kontynuuj</string> + <plurals name="qr_scanner_sequence_progress"> + <item quantity="one">Zdobyto %1$s z %2$s części — kontynuuj</item> + <item quantity="few">Zdobyto %1$s z %2$s części — kontynuuj</item> + <item quantity="many">Zdobyto %1$s z %2$s części — kontynuuj</item> + <item quantity="other">Zdobyto %1$s z %2$s części — kontynuuj</item> + </plurals> <string name="qr_scanner_zoom_reset">Zresetuj zoom</string> <string name="qr_scanner_unsupported_title">Ametyst nie może otworzyć tego kodu</string> <string name="qr_scanner_unsupported_secret">Ten kod zawiera klucz prywatny lub hasło parowania, dlatego jego treść nie jest tutaj wyświetlana. Należy skorzystać z odpowiedniego ekranu, na którym należy go wprowadzić.</string> @@ -5475,16 +5489,35 @@ <string name="cyberspace_bag_cancel">Stop</string> <string name="cyberspace_bag_measuring">Wycena wyszukiwania na tym urządzeniu…</string> <string name="cyberspace_bag_progress">Przeszukano %1$s z %2$s regionów</string> - <string name="cyberspace_bag_too_slow_minutes">Poza zasięgiem: około %1$d minut wyszukiwania na tym urządzeniu</string> - <string name="cyberspace_bag_too_slow_hours">Poza zasięgiem: około %1$d godzin wyszukiwania na tym urządzeniu</string> + <plurals name="cyberspace_bag_too_slow_minutes"> + <item quantity="one">Poza zasięgiem: około %1$d minuty wyszukiwania na tym urządzeniu</item> + <item quantity="few">Poza zasięgiem: około %1$d minut wyszukiwania na tym urządzeniu</item> + <item quantity="many">Poza zasięgiem: około %1$d minut wyszukiwania na tym urządzeniu</item> + <item quantity="other">Poza zasięgiem: około %1$d minuty wyszukiwania na tym urządzeniu</item> + </plurals> + <plurals name="cyberspace_bag_too_slow_hours"> + <item quantity="one">Poza zasięgiem: około %1$d godziny wyszukiwania na tym urządzeniu</item> + <item quantity="few">Poza zasięgiem: około %1$d godzin wyszukiwania na tym urządzeniu</item> + <item quantity="many">Poza zasięgiem: około %1$d godzin wyszukiwania na tym urządzeniu</item> + <item quantity="other">Poza zasięgiem: około %1$d godziny wyszukiwania na tym urządzeniu</item> + </plurals> <string name="cyberspace_bag_not_found">W pudełku nie ma podpowiedzi o nazwie</string> <string name="cyberspace_bag_damaged">Znaleziono region, ale nie udało się odczytać jego zawartości</string> - <string name="cyberspace_bag_dropped">%1$d element ukryty w tym miejscu nie pasował do swojej sygnatury podpisu i został usunięty</string> - <string name="cyberspace_bag_dropped_many">%1$d ukrytych tutaj elementów nie pasowało do swoich sygnatur i zostało usuniętych</string> + <plurals name="cyberspace_bag_dropped"> + <item quantity="one">%1$d element ukryty w tym miejscu nie pasował do swojej sygnatury podpisu i został usunięty</item> + <item quantity="few">%1$d elementy ukryte tutaj nie pasowały do swoich sygnatur i zostały usunięte</item> + <item quantity="many">%1$d ukrytych tutaj elementów nie pasowało do swoich sygnatur i zostało usuniętych</item> + <item quantity="other">%1$d ukrytego tutaj elementu nie pasowało do swoich sygnatur i zostało usuniętych</item> + </plurals> <string name="cyberspace_bag_empty">Otworzyłem, a w środku nic nie było</string> <string name="cyberspace_bag_unsigned">Brak podpisu, więc autor to tylko domysł</string> <string name="cyberspace_bag_unknown_kind">Element typu %1$d, którego ten klient nie wyświetla</string> - <string name="cyberspace_bag_opaque">%1$d bajtów danych, których ten klient nie odczytuje</string> + <plurals name="cyberspace_bag_opaque"> + <item quantity="one">%1$d bajt danych, których ten klient nie odczytuje</item> + <item quantity="few">%1$d bajty danych, których ten klient nie odczytuje</item> + <item quantity="many">%1$d bajtów danych, których ten klient nie odczytuje</item> + <item quantity="other">%1$d bajta danych, których ten klient nie odczytuje</item> + </plurals> <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> <string name="backup_action_block_again">Zablokuj %1$s ponownie</string> <string name="backup_action_keep_count">Zachowaj %1$s</string> @@ -5783,13 +5816,23 @@ <string name="backup_review_renamed">zmieniono nazwę</string> <string name="backup_review_renamed_tag">Zmieniono nazwę</string> <string name="backup_review_saved">Na tym urządzeniu</string> - <string name="backup_review_search_people">Szukaj %1$s osób</string> + <plurals name="backup_review_search_people"> + <item quantity="one">Szukaj %1$s osoby</item> + <item quantity="few">Szukaj %1$s osób</item> + <item quantity="many">Szukaj %1$s osób</item> + <item quantity="other">Szukaj %1$s osoby</item> + </plurals> <string name="backup_review_searching_tag">Szukam</string> <string name="backup_review_service">Serwis</string> <string name="backup_review_service_followers">Obserwujący</string> <string name="backup_review_service_rank">Ranking</string> <string name="backup_review_service_topics">Tematy</string> - <string name="backup_review_services_changed">Zmieniono %1$s z %2$s serwisów</string> + <plurals name="backup_review_services_changed"> + <item quantity="one">Zmieniono %1$s z %2$s serwisu</item> + <item quantity="few">Zmieniono %1$s z %2$s serwisów</item> + <item quantity="many">Zmieniono %1$s z %2$s serwisów</item> + <item quantity="other">Zmieniono %1$s z %2$s serwisu</item> + </plurals> <string name="backup_review_services_changed_explainer">Teraz pochodzą od kogoś innego albo od nikogo</string> <string name="backup_review_still_blocked">Nadal zablokowana</string> <string name="backup_review_storing_tag">Przechowywanie</string> diff --git a/commonsUI/src/commonMain/composeResources/values/strings.xml b/commonsUI/src/commonMain/composeResources/values/strings.xml index e1a0e9cd1e..3782cc7eb2 100644 --- a/commonsUI/src/commonMain/composeResources/values/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values/strings.xml @@ -219,7 +219,10 @@ <string name="geocache_hunt_new">New hunt…</string> <string name="geocache_hunt_none_yet">You have not made a hunt yet.</string> <string name="geocache_hunt_already_on">Already on this hunt</string> - <string name="geocache_caches_here">%1$d caches here</string> + <plurals name="geocache_caches_here"> + <item quantity="one">%1$d cache here</item> + <item quantity="other">%1$d caches here</item> + </plurals> <string name="geocache_photo_remove">Remove this photo</string> <string name="geocache_archived_notice">This cache has been archived. It is no longer in play.</string> <string name="geocache_ftf_won_by">First to find claimed by %1$s</string> @@ -278,7 +281,6 @@ <string name="geocache_map_recenter">Centre on me</string> <string name="geocache_map_long_press">Long-press the map to hide a cache there</string> <string name="geocache_map_open">Open</string> - <string name="geocache_map_pin_count">%1$d caches</string> <string name="geocache_new_section_where">Where</string> <string name="geocache_new_section_what">What</string> <string name="geocache_new_section_extras">Extras</string> @@ -306,7 +308,10 @@ <string name="geocache_new_needs_location">Pick where the cache is hidden.</string> <string name="geocache_hunt_progress">%1$d of %2$d found</string> <string name="geocache_hunt_start">Start the hunt</string> - <string name="geocache_hunt_caches">%1$d caches</string> + <plurals name="geocache_hunt_caches"> + <item quantity="one">%1$d cache</item> + <item quantity="other">%1$d caches</item> + </plurals> <string name="geocache_hunt_title">Title</string> <string name="geocache_hunt_description">Description</string> <string name="geocache_hunt_banner">Banner image</string> @@ -355,7 +360,10 @@ <string name="qr_scanner_no_code_in_image">No QR code found in that picture</string> <string name="qr_scanner_clipboard_empty">Nothing on the clipboard to scan</string> <string name="qr_scanner_pick_one">Tap the code you want</string> - <string name="qr_scanner_sequence_progress">Captured %1$s of %2$s parts — keep going</string> + <plurals name="qr_scanner_sequence_progress"> + <item quantity="one">Captured %1$s of %2$s part — keep going</item> + <item quantity="other">Captured %1$s of %2$s parts — keep going</item> + </plurals> <string name="qr_scanner_zoom_reset">Reset zoom</string> <string name="qr_scanner_unsupported_title">Amethyst can't open this code</string> <string name="qr_scanner_unsupported_secret">This code carries a private key or a pairing secret, so its contents are not shown here. Use the screen that expects it.</string> @@ -5324,7 +5332,10 @@ <string name="cordn_health_unknown">Not contacted yet</string> <string name="cordn_health_ok">Responding</string> <string name="cordn_health_down">Not responding</string> - <string name="cordn_health_failures">%1$d failed attempts in a row</string> + <plurals name="cordn_health_failures"> + <item quantity="one">%1$d failed attempt in a row</item> + <item quantity="other">%1$d failed attempts in a row</item> + </plurals> <string name="cordn_link_explainer">Paste a cordn1… link someone shared with you to see which coordinator carries that group, and what that operator would learn about you if you joined.</string> <string name="cordn_link_field">cordn1…</string> @@ -5420,7 +5431,10 @@ <string name="cordn_migrate_import">Bring the groups here</string> <string name="cordn_migrate_importing">Fetching…</string> <string name="cordn_migrate_replaces_warning">This replaces any cordn groups already on this phone. They cannot be merged.</string> - <string name="cordn_migrate_done">%1$d groups moved</string> + <plurals name="cordn_migrate_done"> + <item quantity="one">%1$d group moved</item> + <item quantity="other">%1$d groups moved</item> + </plurals> <string name="cordn_migrate_no_groups">There are no cordn groups on this device to move.</string> <string name="cordn_migrate_no_server">Set a media server first — the encrypted documents need somewhere to sit while the other phone fetches them.</string> <string name="cordn_migrate_exposure_title">What leaves this device</string> @@ -5446,16 +5460,27 @@ <string name="cyberspace_bag_cancel">Stop</string> <string name="cyberspace_bag_measuring">Pricing the search on this device…</string> <string name="cyberspace_bag_progress">Searched %1$s of %2$s regions</string> - <string name="cyberspace_bag_too_slow_minutes">Out of reach: about %1$d minutes of searching on this device</string> - <string name="cyberspace_bag_too_slow_hours">Out of reach: about %1$d hours of searching on this device</string> + <plurals name="cyberspace_bag_too_slow_minutes"> + <item quantity="one">Out of reach: about %1$d minute of searching on this device</item> + <item quantity="other">Out of reach: about %1$d minutes of searching on this device</item> + </plurals> + <plurals name="cyberspace_bag_too_slow_hours"> + <item quantity="one">Out of reach: about %1$d hour of searching on this device</item> + <item quantity="other">Out of reach: about %1$d hours of searching on this device</item> + </plurals> <string name="cyberspace_bag_not_found">Not in the box the hint named</string> <string name="cyberspace_bag_damaged">Found the region, but the contents could not be read</string> - <string name="cyberspace_bag_dropped">%1$d item hidden here did not match its signature and was dropped</string> - <string name="cyberspace_bag_dropped_many">%1$d items hidden here did not match their signatures and were dropped</string> + <plurals name="cyberspace_bag_dropped"> + <item quantity="one">%1$d item hidden here did not match its signature and was dropped</item> + <item quantity="other">%1$d items hidden here did not match their signatures and were dropped</item> + </plurals> <string name="cyberspace_bag_empty">Opened, and there was nothing inside</string> <string name="cyberspace_bag_unsigned">Unsigned, so this author is a claim</string> <string name="cyberspace_bag_unknown_kind">An item of kind %1$d, which this client does not draw</string> - <string name="cyberspace_bag_opaque">%1$d bytes of something this client does not read</string> + <plurals name="cyberspace_bag_opaque"> + <item quantity="one">%1$d byte of something this client does not read</item> + <item quantity="other">%1$d bytes of something this client does not read</item> + </plurals> <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> <string name="browser_pill_close">Close</string> <string name="browser_pill_tile_copy">Copy link</string> @@ -5800,13 +5825,19 @@ <string name="backup_review_renamed">renamed</string> <string name="backup_review_renamed_tag">Renamed</string> <string name="backup_review_saved">On this device</string> - <string name="backup_review_search_people">Search %1$s people</string> + <plurals name="backup_review_search_people"> + <item quantity="one">Search %1$s person</item> + <item quantity="other">Search %1$s people</item> + </plurals> <string name="backup_review_searching_tag">Searching</string> <string name="backup_review_service">Service</string> <string name="backup_review_service_followers">Followers</string> <string name="backup_review_service_rank">Rank</string> <string name="backup_review_service_topics">Topics</string> - <string name="backup_review_services_changed">%1$s of %2$s services changed</string> + <plurals name="backup_review_services_changed"> + <item quantity="one">%1$s of %2$s service changed</item> + <item quantity="other">%1$s of %2$s services changed</item> + </plurals> <string name="backup_review_services_changed_explainer">They now come from someone else, or no one</string> <string name="backup_review_still_blocked">Still blocked</string> <string name="backup_review_storing_tag">Storing</string> @@ -5924,9 +5955,15 @@ <string name="cordn_coordinators_discover_none">Nobody is announcing on your relays.</string> <string name="cordn_coordinators_discover_seen">Announced %1$s ago</string> <string name="cordn_coordinators_discover_seen_on">Last announced %1$s</string> - <string name="cordn_coordinators_discover_unheard">Nothing found, and %1$d of your relays did not answer.</string> + <plurals name="cordn_coordinators_discover_unheard"> + <item quantity="one">Nothing found, and %1$d of your relays did not answer.</item> + <item quantity="other">Nothing found, and %1$d of your relays did not answer.</item> + </plurals> <string name="cordn_coordinators_explainer">A coordinator orders one group's messages and is the only thing that can. Losing it loses the ordering, and a second one does not mirror the first — so these are named things you choose, not interchangeable relays.</string> - <string name="cordn_coordinators_health_down">Not answering — %1$d calls in a row have failed. Groups on it are not syncing.</string> + <plurals name="cordn_coordinators_health_down"> + <item quantity="one">Not answering — %1$d call in a row has failed. Groups on it are not syncing.</item> + <item quantity="other">Not answering — %1$d calls in a row have failed. Groups on it are not syncing.</item> + </plurals> <string name="cordn_coordinators_health_ok">Answering.</string> <string name="cordn_coordinators_health_retrying">A call failed; retrying.</string> <string name="cordn_coordinators_health_unknown">Nothing asked of it yet.</string> @@ -5950,7 +5987,10 @@ <string name="cordn_coordinators_server_silent">Answered, but named nothing.</string> <string name="cordn_coordinators_show_all">Show all %1$d</string> <string name="cordn_coordinators_show_fewer">Show fewer</string> - <string name="cordn_coordinators_show_older">Show %1$d that stopped announcing</string> + <plurals name="cordn_coordinators_show_older"> + <item quantity="one">Show %1$d that stopped announcing</item> + <item quantity="other">Show %1$d that stopped announcing</item> + </plurals> <string name="cordn_coordinators_stale_note">These have not announced in over a month. Creating a group on one that has gone away will fail.</string> <string name="cordn_create_action">Create group</string> <plurals name="cordn_create_action_invite"> @@ -5972,7 +6012,10 @@ <string name="cordn_create_coordinator_new">Another coordinator</string> <string name="cordn_create_coordinator_pubkey">Coordinator public key (hex or npub)</string> <string name="cordn_create_coordinator_relays">Relays it answers on, one per line</string> - <string name="cordn_create_coverage_all">Reaches all %1$d of your people</string> + <plurals name="cordn_create_coverage_all"> + <item quantity="one">Reaches your %1$d person</item> + <item quantity="other">Reaches all %1$d of your people</item> + </plurals> <string name="cordn_create_coverage_partial">Reaches %1$d of %2$d — the rest would be left out</string> <string name="cordn_create_coverage_unknown">Not checked — picking it will ask</string> <string name="cordn_create_description">Description (optional)</string> diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/cordn/ui/CoordinatorHealthRow.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/cordn/ui/CoordinatorHealthRow.kt index a6770124d3..bcd12f79c7 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/cordn/ui/CoordinatorHealthRow.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/cordn/ui/CoordinatorHealthRow.kt @@ -37,6 +37,7 @@ import com.vitorpamplona.amethyst.commons.resources.cordn_health_down import com.vitorpamplona.amethyst.commons.resources.cordn_health_failures import com.vitorpamplona.amethyst.commons.resources.cordn_health_ok import com.vitorpamplona.amethyst.commons.resources.cordn_health_unknown +import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource /** @@ -97,7 +98,7 @@ fun CoordinatorHealthRow( // deserves no words at all. if (state.isDown) { Text( - text = stringResource(Res.string.cordn_health_failures, state.consecutiveFailures), + text = pluralStringResource(Res.plurals.cordn_health_failures, state.consecutiveFailures, state.consecutiveFailures), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) From 6a3300be3541c1b26f87d94bfc4219e4706fb518 Mon Sep 17 00:00:00 2001 From: davotoula <david.kaspar@gmail.com> Date: Sun, 27 Sep 2026 10:28:15 +0200 Subject: [PATCH 46/60] chore(i18n): translate missing strings into cs, de, sv and pt-BR Fills every key that was absent from the four target locales: - commonsUI: 996 entries per locale (938 strings, 58 plurals), mostly the cordn, backup, geocache and browser features. - amethyst: the 4 platform-tier keys (QR share target and launcher shortcut labels, PoW "sending" notification). Plurals carry each locale's full category set (cs one/few/many/other). Pure format values (D%1$d, %1$02d:%2$02d, ...) are copied verbatim so the Crowdin upload seeds them for approval. --- amethyst/src/main/res/values-cs/strings.xml | 4 + .../src/main/res/values-de-rDE/strings.xml | 4 + .../src/main/res/values-pt-rBR/strings.xml | 4 + .../src/main/res/values-sv-rSE/strings.xml | 4 + .../composeResources/values-cs/strings.xml | 1286 +++++++++++++++++ .../values-de-rDE/strings.xml | 1170 +++++++++++++++ .../values-pt-rBR/strings.xml | 1170 +++++++++++++++ .../values-sv-rSE/strings.xml | 1170 +++++++++++++++ 8 files changed, 4812 insertions(+) diff --git a/amethyst/src/main/res/values-cs/strings.xml b/amethyst/src/main/res/values-cs/strings.xml index c56adcaead..7130fe54f3 100644 --- a/amethyst/src/main/res/values-cs/strings.xml +++ b/amethyst/src/main/res/values-cs/strings.xml @@ -377,4 +377,8 @@ <string name="mute_button">Zvuk zapnutý. Klikněte pro ztlumení</string> <string name="play">Hrát</string> <string name="pause">Pozastavit</string> + <string name="share_target_scan_qr">Skenovat QR kód</string> + <string name="shortcut_scan_qr_short">Skenovat QR</string> + <string name="shortcut_scan_qr_long">Naskenovat QR kód</string> + <string name="pow_notification_sending">Odesílání na relé…</string> </resources> diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index c46ed3312d..2e4d479622 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -349,4 +349,8 @@ <string name="mute_button">Ton ein. Zur Stummschaltung drücken</string> <string name="play">Abspielen</string> <string name="pause">Pausen</string> + <string name="share_target_scan_qr">QR-Code scannen</string> + <string name="shortcut_scan_qr_short">QR scannen</string> + <string name="shortcut_scan_qr_long">Einen QR-Code scannen</string> + <string name="pow_notification_sending">Wird an Relays gesendet…</string> </resources> diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 53f568feea..af84b30244 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -349,4 +349,8 @@ <string name="mute_button">Som ligado. Clique para silenciar</string> <string name="play">Reproduzir</string> <string name="pause">Pausar</string> + <string name="share_target_scan_qr">Escanear código QR</string> + <string name="shortcut_scan_qr_short">Ler QR</string> + <string name="shortcut_scan_qr_long">Escanear um código QR</string> + <string name="pow_notification_sending">Enviando para relays…</string> </resources> diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 310c1657a6..294ee30c6f 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -349,4 +349,8 @@ <string name="mute_button">Ljud på. Klicka för att stänga av ljudet</string> <string name="play">Spela</string> <string name="pause">Pausa</string> + <string name="share_target_scan_qr">Skanna QR-kod</string> + <string name="shortcut_scan_qr_short">Skanna QR</string> + <string name="shortcut_scan_qr_long">Skanna en QR-kod</string> + <string name="pow_notification_sending">Skickar till reläer…</string> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-cs/strings.xml b/commonsUI/src/commonMain/composeResources/values-cs/strings.xml index 3f818c83b8..eb86dff2d2 100644 --- a/commonsUI/src/commonMain/composeResources/values-cs/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-cs/strings.xml @@ -5140,4 +5140,1290 @@ one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> + <string name="geocache_unnamed">Keš</string> + <string name="geocache_type_traditional">Tradiční</string> + <string name="geocache_type_multi">Multi-keš</string> + <string name="geocache_type_mystery">Mystery</string> + <string name="geocache_size_micro">Mikro</string> + <string name="geocache_size_small">Malá</string> + <string name="geocache_size_regular">Normální</string> + <string name="geocache_size_large">Velká</string> + <string name="geocache_size_other">Jiná velikost</string> + <string name="geocache_difficulty_short">D%1$d</string> + <string name="geocache_terrain_short">T%1$d</string> + <string name="geocache_first_to_find">První nálezce</string> + <string name="geocache_is_art">Umělecké dílo</string> + <string name="geocache_mission">Key Quest</string> + <string name="geocache_needs_verification">Ověřené nálezy</string> + <string name="geocache_claimed">Obsazeno</string> + <string name="geocache_archived">Archivováno</string> + <string name="geocache_hint_tap_to_reveal">Nevíte si rady? Klepnutím zobrazíte nápovědu</string> + <string name="geocache_hint_other_reading">Nečitelné? Klepnutím zobrazíte druhé čtení</string> + <string name="geocache_nearby">Poblíž</string> + <string name="geocache_found_it">Nalezeno!</string> + <string name="geocache_verified_find">Ověřený nález</string> + <string name="geocache_invalid_proof">Důkaz nesouhlasí</string> + <string name="geocache_loading">Načítání keše…</string> + <string name="geocache_hidden_by">Ukryl %1$s</string> + <string name="geocache_navigate">Navigovat</string> + <string name="geocache_copy_coordinates">Kopírovat souřadnice</string> + <string name="geocache_coordinates_copied">Souřadnice zkopírovány</string> + <string name="geocache_photos_section">Fotky</string> + <string name="geocache_mission_section">Mise Key Quest</string> + <string name="geocache_logs_section">Logy</string> + <string name="geocache_no_logs">Tuto keš zatím nikdo nezalogoval. Buďte první.</string> + <string name="geocache_i_found_it">Nalezeno</string> + <string name="geocache_didnt_find_it">Nenalezeno</string> + <string name="geocache_add_note">Přidat poznámku</string> + <string name="geocache_needs_maintenance">Nahlásit potřebu údržby</string> + <string name="geocache_add_to_hunt">Přidat do výpravy</string> + <string name="geocache_hunt_new">Nová výprava…</string> + <string name="geocache_hunt_none_yet">Zatím jste nevytvořili žádnou výpravu.</string> + <string name="geocache_hunt_already_on">Už je v této výpravě</string> + <plurals name="geocache_caches_here"> + <item quantity="one">%1$d keš zde</item> + <item quantity="few">%1$d keše zde</item> + <item quantity="many">%1$d keše zde</item> + <item quantity="other">%1$d keší zde</item> + </plurals> + <string name="geocache_photo_remove">Odebrat tuto fotku</string> + <string name="geocache_archived_notice">Tato keš byla archivována. Už není ve hře.</string> + <string name="geocache_ftf_won_by">Prvním nálezcem se stal %1$s</string> + <string name="geocache_ftf_available">Místo prvního nálezce je stále volné</string> + <string name="geocache_owner_actions">Vlastník</string> + <string name="geocache_owner_edit">Upravit tuto keš</string> + <string name="geocache_owner_archive">Archivovat tuto keš</string> + <string name="geocache_owner_archive_confirm">Archivovat. Nálezci uvidí, že už není ve hře, a listing zůstane čitelný.</string> + <string name="geocache_owner_lock_ftf">Potvrdit prvního nálezce</string> + <string name="geocache_owner_lock_ftf_explain">Zveřejní tag F se jménem %1$s. Je to trvalé.</string> + <string name="geocache_owner_no_finders">Zatím žádné ověřené nálezy, takže není koho potvrdit.</string> + <string name="geocache_owner_show_qr">Zobrazit ověřovací kód</string> + <string name="geocache_owner_qr_warning">Vytiskněte ho a vložte do keše. Amethyst ho neukládá — pokud ho ztratíte, nálezci už nebudou moci prokázat, že tam byli.</string> + <string name="geocache_log_type_dnf">Nenalezeno</string> + <string name="geocache_log_type_note">Poznámka</string> + <string name="geocache_log_type_maintenance">Potřebuje údržbu</string> + <string name="geocache_log_title">Zapsat nález</string> + <string name="geocache_log_cache_label">Keš</string> + <string name="geocache_log_your_log">Váš log</string> + <string name="geocache_log_placeholder">Jak to šlo?</string> + <string name="geocache_log_add_photo">Přidat fotku</string> + <string name="geocache_log_mission_answer">Vaše odpověď Key Quest</string> + <string name="geocache_log_proof_section">Důkaz přítomnosti</string> + <string name="geocache_log_scan_code">Naskenujte kód v keši</string> + <string name="geocache_log_scan_again">Naskenovat jiný kód</string> + <string name="geocache_log_proof_verified">Ověřeno — byli jste u keše</string> + <string name="geocache_log_proof_explain">Jednou podepsáno kódem z krabičky. Klíč se nikdy neukládá a nikdy neopustí tuto obrazovku.</string> + <string name="geocache_log_proof_bad_code">To není ověřovací kód.</string> + <string name="geocache_log_proof_wrong_cache">Tento kód patří jiné keši.</string> + <string name="geocache_log_proof_optional">Tato keš nevyžaduje důkaz, takže ji můžete rovnou zalogovat.</string> + <string name="geocache_log_no_location_attached">K tomuto logu není připojena vaše vlastní poloha.</string> + <string name="geocache_log_post">Odeslat</string> + <string name="geocache_log_comment_title">Přidat log</string> + <string name="geocache_tab_nearby">Poblíž</string> + <string name="geocache_tab_map">Mapa</string> + <string name="geocache_tab_hunts">Výpravy</string> + <string name="geocache_tab_finds">Nálezy</string> + <string name="geocache_tab_mine">Moje</string> + <string name="geocache_filter_unfound">Nenalezené</string> + <string name="geocache_filter_difficulty">D ≤ %1$d</string> + <string name="geocache_filter_terrain">T ≤ %1$d</string> + <string name="geocache_filter_has_prize">S cenou</string> + <string name="geocache_filter_verified_only">Vyžaduje důkaz</string> + <string name="geocache_use_my_location">Použít mou polohu</string> + <string name="geocache_sorted_by_distance">Seřazeno podle vzdálenosti</string> + <string name="geocache_sorted_by_recent">Nejnovější první</string> + <string name="geocache_hide_a_cache">Ukrýt keš</string> + <string name="geocache_empty_nearby">Zatím tu nejsou žádné keše. Zkuste širší seznam sledovaných, nebo ukryjte první.</string> + <string name="geocache_empty_finds">Zatím jste nezapsali žádný nález.</string> + <string name="geocache_empty_mine">Zatím jste neukryli žádnou keš.</string> + <string name="geocache_empty_hunts">Zatím žádné výpravy.</string> + <string name="geocache_stat_finds">Nálezy</string> + <string name="geocache_stat_ftf">První nálezy</string> + <string name="geocache_stat_verified">Ověřené</string> + <string name="geocache_stat_hidden">Ukryté</string> + <string name="geocache_map_recenter">Vycentrovat na mě</string> + <string name="geocache_map_long_press">Dlouhým stiskem na mapě tam ukryjete keš</string> + <string name="geocache_map_open">Otevřít</string> + <string name="geocache_new_section_where">Kde</string> + <string name="geocache_new_section_what">Co</string> + <string name="geocache_new_section_extras">Doplňky</string> + <string name="geocache_new_section_proof">Důkaz</string> + <string name="geocache_new_section_relays">Relaye</string> + <string name="geocache_new_location_warning">Tímto zveřejníte veřejnou polohu s přesností asi na 5 metrů. Uvidí ji kdokoli na Nostru a zveřejněnou událost nelze vzít zpět.</string> + <string name="geocache_new_pick_location">Vyberte místo</string> + <string name="geocache_new_ladder">Zveřejní %1$s a k tomu hrubší buňky až po 3 znaky pro vyhledávání v okolí.</string> + <string name="geocache_new_name">Název</string> + <string name="geocache_new_description">Popis</string> + <string name="geocache_new_type">Typ</string> + <string name="geocache_new_size">Velikost</string> + <string name="geocache_new_difficulty">Obtížnost</string> + <string name="geocache_new_terrain">Terén</string> + <string name="geocache_new_hint">Nápověda</string> + <string name="geocache_new_hint_preview">Nálezci uvidí</string> + <string name="geocache_new_hint_scramble">Zašifrovat nápovědu (ROT13)</string> + <string name="geocache_new_mission">Mise Key Quest</string> + <string name="geocache_new_modifiers">Modifikátory</string> + <string name="geocache_new_require_proof">Vyžadovat důkaz přítomnosti</string> + <string name="geocache_new_proof_ready">Pro tuto keš byl vygenerován ověřovací klíč.</string> + <string name="geocache_new_log_relays">Kam mají nálezci logovat</string> + <string name="geocache_new_publish">Zveřejnit</string> + <string name="geocache_new_needs_name">Pojmenujte keš.</string> + <string name="geocache_new_needs_location">Vyberte, kde je keš ukrytá.</string> + <string name="geocache_hunt_progress">Nalezeno %1$d z %2$d</string> + <string name="geocache_hunt_start">Zahájit výpravu</string> + <plurals name="geocache_hunt_caches"> + <item quantity="one">%1$d keš</item> + <item quantity="few">%1$d keše</item> + <item quantity="many">%1$d keše</item> + <item quantity="other">%1$d keší</item> + </plurals> + <string name="geocache_hunt_title">Název</string> + <string name="geocache_hunt_description">Popis</string> + <string name="geocache_hunt_banner">Obrázek banneru</string> + <string name="geocache_hunt_pick_caches">Keše v pořadí</string> + <string name="geocache_hunt_add_cache">Přidat keš</string> + <string name="geocache_hunt_move_up">Posunout nahoru</string> + <string name="geocache_hunt_move_down">Posunout dolů</string> + <string name="geocache_hunt_remove">Odebrat</string> + <string name="geocache_hunt_theme">Motiv stránky</string> + <string name="geocache_hunt_map_style">Styl mapy</string> + <string name="geocache_hunt_needs_title">Pojmenujte výpravu.</string> + <string name="geocache_hunt_needs_caches">Přidejte alespoň jednu keš.</string> + <string name="geocache_publish_failed">Zveřejnění se nezdařilo. Zkontrolujte připojení k relayům a zkuste to znovu.</string> + <string name="cordn_groups_title">Skupiny Cordn</string> + <string name="cordn_badge_coordinator">Skupina s koordinátorem</string> + <string name="cordn_badge_coordinator_desc">Doručováno koordinátorem, ne relayi</string> + <string name="cordn_exposure_title">Co tento koordinátor vidí</string> + <string name="cordn_exposure_subtitle">Koordinátor je jediný server, který přenáší všechny zprávy této skupiny, a to v pořadí. Je to jiný kompromis než u relayů — dobré to vědět, než s tím začnete zacházet jako s jakýmkoli jiným chatem.</string> + <string name="cordn_exposure_dimension_content">Obsah zpráv</string> + <string name="cordn_exposure_dimension_membership">Kdo je ve skupině</string> + <string name="cordn_exposure_dimension_messaging">Kdo co posílá</string> + <string name="cordn_exposure_level_none">Nečitelné</string> + <string name="cordn_exposure_level_pseudonymous">Pod jednorázovým klíčem</string> + <string name="cordn_exposure_level_identified">Spojeno s vaším skutečným účtem</string> + <string name="cordn_exposure_details">Podrobnosti</string> + <string name="cordn_exposure_coordinator">Koordinátor %1$s</string> + <string name="cordn_exposure_differs">Nejde o stejnou míru odhalení jako u skupiny Marmot, kde žádný jednotlivý provozovatel nedrží celou konverzaci.</string> + <string name="cordn_note_membership">Připojení uvádí skutečné účty na obou stranách, takže koordinátor ví, kdo je v této skupině.</string> + <string name="cordn_note_linked">Jeden jednorázový klíč odesílá a stahuje za všechny vaše skupiny zde, takže koordinátor pozná, že patří vám.</string> + <string name="cordn_note_history">Jediný provozovatel drží úplnou, seřazenou historii každé skupiny, kterou přenáší.</string> + <string name="cordn_note_publication">Balíček klíčů, který jste zveřejnili, je podepsaný záznam o tom, že tento účet používá cordn, a koordinátor ho může předat komukoli.</string> + <string name="cordn_note_sizes">Zprávy nejsou doplňovány na jednotnou délku, takže koordinátor vidí, jak dlouhá je každá z nich.</string> + <string name="cordn_note_encryption_bug">Tento klient nešifruje komunikaci s koordinátorem. Je to chyba — nahlaste ji prosím.</string> + <string name="cordn_health_unknown">Zatím nekontaktován</string> + <string name="cordn_health_ok">Odpovídá</string> + <string name="cordn_health_down">Neodpovídá</string> + <plurals name="cordn_health_failures"> + <item quantity="one">%1$d neúspěšný pokus za sebou</item> + <item quantity="few">%1$d neúspěšné pokusy za sebou</item> + <item quantity="many">%1$d neúspěšného pokusu za sebou</item> + <item quantity="other">%1$d neúspěšných pokusů za sebou</item> + </plurals> + <string name="cordn_link_explainer">Vložte odkaz cordn1…, který vám někdo poslal, a zjistěte, který koordinátor tuto skupinu přenáší a co by se o vás jeho provozovatel dozvěděl, kdybyste se připojili.</string> + <string name="cordn_link_field">cordn1…</string> + <string name="cordn_link_inspect">Prohlédnout</string> + <string name="cordn_link_paste">Vložit</string> + <string name="cordn_link_clear">Vymazat</string> + <string name="cordn_link_invalid">To není odkaz na skupinu cordn: %1$s</string> + <string name="cordn_link_group_id">ID skupiny</string> + <string name="cordn_link_coordinator">Koordinátor</string> + <string name="cordn_link_relays">Dostupné přes</string> + <string name="cordn_link_no_coordinator">Tento odkaz neuvádí žádného koordinátora, takže ho samotný nelze použít. Zeptejte se odesílatele, který koordinátor skupinu přenáší.</string> + <string name="cordn_link_not_joinable">Prohlédnutím odkazu se k ničemu nepřipojíte a to, že ho máte, z vás nedělá člena.</string> + <string name="cordn_link_title">Odkaz na skupinu Cordn</string> + <string name="cordn_link_search_keywords">cordn koordinátor skupina odkaz pozvánka mls chat metadata soukromí odhalení coordinator group link invite privacy exposure</string> + <string name="cordn_coordinators_title">Koordinátoři Cordn</string> + <string name="cordn_coordinators_search_keywords">cordn koordinátor přidat odebrat vymazat stav server mls chat pořadí coordinator add remove purge health ordering</string> + <string name="cordn_keypackages_title">Balíčky klíčů Cordn</string> + <string name="cordn_keypackages_search_keywords">cordn balíček klíčů zveřejnit pozvánka záložní koordinátor mls key package publish invite last resort coordinator</string> + <string name="cordn_backup_title">Záloha Cordn</string> + <string name="cordn_backup_search_keywords">cordn záloha obnovení export import heslo skupiny obnova backup restore passphrase groups recovery</string> + <string name="cordn_group_untitled">Skupina %1$s</string> + <string name="cordn_group_no_messages_yet">Zatím žádné zprávy</string> + <string name="cordn_group_via_coordinator">Skupina Cordn přenášená přes %1$s</string> + <string name="cordn_preview_deleted">Zpráva smazána</string> + <string name="cordn_preview_voice_note">Hlasová zpráva</string> + <string name="cordn_preview_photo">Fotka</string> + <string name="cordn_preview_video">Video</string> + <string name="cordn_preview_file">Soubor</string> + <string name="cordn_invitations_entry">Máte pozvánku do skupiny cordn?</string> + <string name="cordn_invitations_entry_action">Zobrazit pozvánky</string> + <string name="cordn_hub_title">Cordn</string> + <string name="cordn_hub_explainer">Skupiny doručované koordinátorem, kterého si sami zvolíte, místo relayů.</string> + <string name="cordn_hub_link">Prohlédnout odkaz na skupinu</string> + <string name="cordn_hub_link_desc">Zjistěte, kam odkaz cordn1… vede, dřív než s ním něco uděláte</string> + <string name="cordn_migrate_section_send">Přesunout z tohoto telefonu</string> + <string name="cordn_migrate_section_receive">Převzít z jiného telefonu</string> + <string name="cordn_keypackages_section">Zveřejněné balíčky klíčů</string> + <string name="cordn_link_section_input">Odkaz</string> + <string name="cordn_coordinators_section_yours">Koordinátoři, které používáte</string> + <string name="cordn_coordinators_section_discover">Ohlašující se koordinátoři</string> + <string name="cordn_coordinators_section_manual">Přidat ručně</string> + <string name="cordn_backup_section_passphrase">Heslo</string> + <string name="cordn_backup_section_export">Vytvořit zálohu</string> + <string name="cordn_backup_section_restore">Obnovit ze zálohy</string> + <string name="cordn_hub_section_service">Koordinátor</string> + <string name="cordn_hub_section_device">Toto zařízení</string> + <string name="cordn_hub_coordinators">Koordinátoři</string> + <string name="cordn_hub_coordinators_desc">Servery, které přenášejí vaše skupiny</string> + <string name="cordn_hub_keypackages">Balíčky klíčů</string> + <string name="cordn_hub_keypackages_desc">Díky nim vás lidé mohou přidat do skupiny</string> + <string name="cordn_hub_backup">Záloha a obnovení</string> + <string name="cordn_hub_backup_desc">Soubor zašifrovaný heslem, který si uschováte</string> + <string name="cordn_hub_migrate">Přesun na nový telefon</string> + <string name="cordn_hub_migrate_desc">Předejte své skupiny zařízení, na které přecházíte</string> + <string name="cordn_migrate_title">Přesun na nový telefon</string> + <string name="cordn_migrate_explainer">Vaše skupiny cordn žijí na tomto zařízení. K jejich přesunu stačí jeden kód: tento telefon je zveřejní, nový telefon je načte a tento telefon s nimi přestane pracovat.</string> + <string name="cordn_migrate_sign_in_first">Nejprve se na novém telefonu přihlaste stejným účtem. Přesouvají se jen vaše skupiny, nikdy váš klíč.</string> + <string name="cordn_migrate_export">Zahájit předání</string> + <string name="cordn_migrate_exporting">Zveřejňování…</string> + <string name="cordn_migrate_scan_this">Naskenujte tento kód na novém telefonu</string> + <string name="cordn_migrate_code_note">Kód je jen ukazatel. Kdo ho vyfotí, nic se nedozví — obsah, na který ukazuje, je zašifrovaný pro vás.</string> + <string name="cordn_migrate_handed_off">Toto zařízení předalo své skupiny</string> + <string name="cordn_migrate_handed_off_desc">Přestalo odesílat a přijímat, protože dva telefony odesílající do jedné skupiny by ji rozbily pro všechny její členy. Vaše skupiny na tomto zařízení zůstávají a nic nebylo smazáno.</string> + <string name="cordn_migrate_cancel">Vzít toto zařízení zpět</string> + <string name="cordn_migrate_cancel_desc">Použijte, pokud se přesun nedokončil. Udělejte to jen tehdy, když nový telefon ještě nezačal odesílat.</string> + <string name="cordn_migrate_receive">Přijmout z jiného telefonu</string> + <string name="cordn_migrate_scan">Naskenovat kód</string> + <string name="cordn_migrate_paste">nebo ho vložte</string> + <string name="cordn_migrate_import">Přenést skupiny sem</string> + <string name="cordn_migrate_importing">Stahování…</string> + <string name="cordn_migrate_replaces_warning">Tímto nahradíte všechny skupiny cordn, které už na tomto telefonu jsou. Sloučit je nelze.</string> + <plurals name="cordn_migrate_done"> + <item quantity="one">Přesunuta %1$d skupina</item> + <item quantity="few">Přesunuty %1$d skupiny</item> + <item quantity="many">Přesunuto %1$d skupiny</item> + <item quantity="other">Přesunuto %1$d skupin</item> + </plurals> + <string name="cordn_migrate_no_groups">Na tomto zařízení nejsou žádné skupiny cordn k přesunu.</string> + <string name="cordn_migrate_no_server">Nejprve nastavte mediální server — zašifrované dokumenty musí někde počkat, než si je druhý telefon stáhne.</string> + <string name="cordn_migrate_exposure_title">Co opouští toto zařízení</string> + <string name="cordn_migrate_exposure_body">Stav vašich skupin se zašifruje a nahraje na váš mediální server, aby si ho druhý telefon mohl stáhnout. Server ho nedokáže přečíst, vidí jen velikost a čas. Pokud nechcete nic zanechat, soubory potom smažte.</string> + <string name="cordn_action_delete">Smazat</string> + <string name="cordn_action_edit">Upravit</string> + <string name="cordn_action_editing">Úprava vaší zprávy</string> + <string name="cordn_action_go_to_message">Přejít na zprávu</string> + <string name="cordn_action_pin">Připnout</string> + <string name="cordn_action_reply">Odpovědět</string> + <string name="cordn_action_unpin">Odepnout</string> + <string name="cordn_admin_action_failed">Tuto změnu nebylo možné provést.</string> + <string name="cordn_admin_coordinator_silent">Koordinátor neodpověděl, takže se nic nezměnilo. Zkuste to za chvíli znovu.</string> + <string name="cordn_admin_no_key_package">%1$s na tomto koordinátoru nezveřejnil balíček klíčů, takže ho zatím nelze přidat. Požádejte ho, aby na tomto koordinátoru nejprve otevřel cordn.</string> + <string name="cordn_admin_no_self_remove">Ze skupiny cordn nemůžete odebrat sami sebe.</string> + <string name="cordn_admin_no_welcome">Pro %1$s se nepodařilo zajistit přístup do skupiny, takže přidání bylo zrušeno.</string> + <string name="cordn_admin_not_a_member">%1$s není v této skupině.</string> + <string name="cordn_admin_someone">Tato osoba</string> + <string name="cordn_admin_wrong_key_package">Tento koordinátor poskytl balíček klíčů, který patří někomu jinému, takže %1$s nebyl přidán. Nic se nezměnilo.</string> + <string name="cordn_backup_contents_body">Šifrovací klíče vašich skupin, vaše balíčky klíčů a všechny zprávy v těchto skupinách. Kdokoli soubor otevře, může konverzace číst. Jakmile soubor opustí Amethyst, chrání ho jen heslo.</string> + <string name="cordn_backup_contents_title">Co soubor obsahuje</string> + <string name="cordn_backup_explainer">Váš nsec skupiny cordn neobnoví. Skupina cordn existuje jako šifrovací stav na tomto zařízení a jako seřazený proud zpráv na koordinátoru, který ho nedokáže přečíst — přijdete-li o zařízení bez zálohy, skupina je pryč, včetně všeho, co v ní už zaznělo.</string> + <string name="cordn_backup_export">Exportovat</string> + <string name="cordn_backup_exported">Záloha uložena.</string> + <string name="cordn_backup_failed">Nepovedlo se. Zkontrolujte heslo a soubor.</string> + <string name="cordn_backup_passphrase">Heslo</string> + <string name="cordn_backup_restore">Obnovit</string> + <string name="cordn_backup_restore_body">Obnovení nahradí skupiny cordn na tomto zařízení těmi ze souboru. Je určeno pro telefon, který převzal roli jiného — ne k používání stejných skupin na dvou zařízeních zároveň.</string> + <string name="cordn_backup_restore_confirm_body">Všechny skupiny cordn, které jsou nyní na tomto zařízení, budou odebrány a nahrazeny těmi ze souboru.\n\nNeobnovujte zálohu na druhý telefon, dokud se ten první stále používá. Oba by měly stejné klíče skupin, a jakmile oba odešlou zprávu, skupina se rozbije pro všechny — potichu a nevratně.</string> + <string name="cordn_backup_restore_confirm_title">Nahradit skupiny cordn na tomto zařízení?</string> + <string name="cordn_backup_restore_title">Obnovit</string> + <string name="cordn_backup_restored">Obnoveno.</string> + <string name="cordn_chat_empty_description">Vše, co sem pošlete, je zašifrováno pro tuto skupinu. Koordinátor to předává dál, aniž by to mohl přečíst.</string> + <string name="cordn_chat_empty_title">Zatím žádné zprávy</string> + <string name="cordn_chat_unread_divider">Nové zprávy</string> + <string name="cordn_chat_yesterday">Včera</string> + <string name="cordn_composer_hint">Zpráva</string> + <string name="cordn_coordinators_add_action">Přidat</string> + <string name="cordn_coordinators_add_failed">Tohoto koordinátora se nepodařilo otevřít.</string> + <string name="cordn_coordinators_copy_key">Kopírovat npub koordinátora</string> + <string name="cordn_coordinators_discover_action">Hledat koordinátory</string> + <string name="cordn_coordinators_discover_add">Přidat</string> + <string name="cordn_coordinators_discover_explainer">Čte oznámení, která už vaše relaye přenášejí. Žádný koordinátor není kontaktován, takže se žádný z nich nedozví, že jste hledali.</string> + <string name="cordn_coordinators_discover_failed">Oznámení se nepodařilo načíst</string> + <string name="cordn_coordinators_discover_none">Na vašich relayích se nikdo neohlašuje.</string> + <string name="cordn_coordinators_discover_seen">Ohlášeno před %1$s</string> + <string name="cordn_coordinators_discover_seen_on">Naposledy ohlášeno %1$s</string> + <plurals name="cordn_coordinators_discover_unheard"> + <item quantity="one">Nic nenalezeno a %1$d z vašich relayů neodpověděl.</item> + <item quantity="few">Nic nenalezeno a %1$d z vašich relayů neodpověděly.</item> + <item quantity="many">Nic nenalezeno a %1$d z vašich relayů neodpovědělo.</item> + <item quantity="other">Nic nenalezeno a %1$d z vašich relayů neodpovědělo.</item> + </plurals> + <string name="cordn_coordinators_explainer">Koordinátor určuje pořadí zpráv jedné skupiny a nic jiného to dělat nemůže. Jeho ztrátou se ztratí i pořadí a druhý koordinátor ten první nezrcadlí — proto jde o konkrétní servery, které si zvolíte, ne o zaměnitelné relaye.</string> + <plurals name="cordn_coordinators_health_down"> + <item quantity="one">Neodpovídá — %1$d požadavek za sebou selhal. Skupiny na něm se nesynchronizují.</item> + <item quantity="few">Neodpovídá — %1$d požadavky za sebou selhaly. Skupiny na něm se nesynchronizují.</item> + <item quantity="many">Neodpovídá — %1$d požadavku za sebou selhalo. Skupiny na něm se nesynchronizují.</item> + <item quantity="other">Neodpovídá — %1$d požadavků za sebou selhalo. Skupiny na něm se nesynchronizují.</item> + </plurals> + <string name="cordn_coordinators_health_ok">Odpovídá.</string> + <string name="cordn_coordinators_health_retrying">Požadavek selhal, zkouší se znovu.</string> + <string name="cordn_coordinators_health_unknown">Zatím nebyl o nic požádán.</string> + <string name="cordn_coordinators_hide_older">Skrýt ty, které se přestaly ohlašovat</string> + <string name="cordn_coordinators_identify">Zjistit, kdo to je</string> + <string name="cordn_coordinators_key">Klíč koordinátora</string> + <string name="cordn_coordinators_label">Název (váš vlastní, volitelný)</string> + <string name="cordn_coordinators_label_note">Tento název je váš a zůstává na tomto zařízení. Koordinátor nemůže název prokázat, proto se ho na něj nikdy neptáme.</string> + <string name="cordn_coordinators_more">Další akce</string> + <string name="cordn_coordinators_none">Zatím žádní koordinátoři.</string> + <string name="cordn_coordinators_purge">Vymazat</string> + <string name="cordn_coordinators_purge_body">Odebráním zůstanou skupiny na tomto zařízení, takže po opětovném přidání koordinátora se vrátí s ním. Vymazáním se smažou: klíče skupin, pozice čtení i vaše balíčky klíčů zmizí a do těchto skupin se lze vrátit jen s novou pozvánkou.\n\nKoordinátor se o tom nedozví a ponechá si vše, co už měl.</string> + <string name="cordn_coordinators_purge_title">Vymazat tohoto koordinátora?</string> + <string name="cordn_coordinators_relays">%1$s</string> + <string name="cordn_coordinators_relays_label">Odpovídá na</string> + <string name="cordn_coordinators_relays_more">%1$s +%2$d další</string> + <string name="cordn_coordinators_remove">Odebrat</string> + <string name="cordn_coordinators_rename">Přejmenovat</string> + <string name="cordn_coordinators_server">Tvrdí, že je: %1$s</string> + <string name="cordn_coordinators_server_claim">Název a verzi si koordinátor zvolil sám. Identifikuje ho jen jeho veřejný klíč.</string> + <string name="cordn_coordinators_server_silent">Odpověděl, ale nic neuvedl.</string> + <string name="cordn_coordinators_show_all">Zobrazit vše (%1$d)</string> + <string name="cordn_coordinators_show_fewer">Zobrazit méně</string> + <plurals name="cordn_coordinators_show_older"> + <item quantity="one">Zobrazit %1$d, který se přestal ohlašovat</item> + <item quantity="few">Zobrazit %1$d, které se přestaly ohlašovat</item> + <item quantity="many">Zobrazit %1$d, které se přestaly ohlašovat</item> + <item quantity="other">Zobrazit %1$d, které se přestaly ohlašovat</item> + </plurals> + <string name="cordn_coordinators_stale_note">Tito koordinátoři se neohlásili déle než měsíc. Vytvoření skupiny na takovém, který už zanikl, selže.</string> + <string name="cordn_create_action">Vytvořit skupinu</string> + <plurals name="cordn_create_action_invite"> + <item quantity="one">Vytvořit a pozvat %1$d osobu</item> + <item quantity="few">Vytvořit a pozvat %1$d osoby</item> + <item quantity="many">Vytvořit a pozvat %1$d osoby</item> + <item quantity="other">Vytvořit a pozvat %1$d osob</item> + </plurals> + <string name="cordn_create_add_member">Přidat někoho</string> + <string name="cordn_create_admin">Administrátor</string> + <plurals name="cordn_create_admin_count"> + <item quantity="one">%1$d osoba může přidávat a odebírat</item> + <item quantity="few">%1$d osoby mohou přidávat a odebírat</item> + <item quantity="many">%1$d osoby může přidávat a odebírat</item> + <item quantity="other">%1$d osob může přidávat a odebírat</item> + </plurals> + <string name="cordn_create_admin_only_me">Tuto skupinu spravuji jen já</string> + <string name="cordn_create_admin_only_me_on">Jen vy můžete zvát, odebírat členy nebo měnit údaje skupiny. Později to můžete předat někomu jinému.</string> + <string name="cordn_create_coordinator">Koordinátor</string> + <string name="cordn_create_coordinator_change">Změnit</string> + <string name="cordn_create_coordinator_discover">Najít koordinátory</string> + <string name="cordn_create_coordinator_hide">Hotovo</string> + <string name="cordn_create_coordinator_new">Jiný koordinátor</string> + <string name="cordn_create_coordinator_pubkey">Veřejný klíč koordinátora (hex nebo npub)</string> + <string name="cordn_create_coordinator_relays">Relaye, na kterých odpovídá, jeden na řádek</string> + <plurals name="cordn_create_coverage_all"> + <item quantity="one">Zastihne vaši %1$d osobu</item> + <item quantity="few">Zastihne všechny %1$d vaše osoby</item> + <item quantity="many">Zastihne %1$d vaší osoby</item> + <item quantity="other">Zastihne všech %1$d vašich osob</item> + </plurals> + <string name="cordn_create_coverage_partial">Zastihne %1$d z %2$d — ostatní by zůstali mimo</string> + <string name="cordn_create_coverage_unknown">Neověřeno — výběrem se to zjistí</string> + <string name="cordn_create_description">Popis (volitelné)</string> + <string name="cordn_create_egalitarian">Všichni mohou přidávat a odebírat</string> + <string name="cordn_create_egalitarian_note">Každý ve skupině může zvát, odebírat členy a měnit její údaje. Kterýkoli člen může později jmenovat administrátory, čímž tento režim skončí.</string> + <string name="cordn_create_explainer">Pořadí zpráv ve skupině cordn určuje jeden koordinátor, kterého si zvolíte. Skupina se vytváří na tomto zařízení — koordinátor se o její existenci dozví, až někoho pozvete nebo pošlete zprávu.</string> + <string name="cordn_create_failed">Skupinu se nepodařilo vytvořit.</string> + <string name="cordn_create_name">Název skupiny</string> + <string name="cordn_create_no_coordinator">Není zvolen žádný koordinátor</string> + <string name="cordn_create_outcome_explainer">Každá pozvánka je zpráva Welcome ponechaná na koordinátoru, takže mohou selhat i jednotlivě. Nikdo nedostal oznámení: pozvánku uvidí, až příště otevřou svou aplikaci.</string> + <string name="cordn_create_outcome_failed">Koordinátor pozvánku odmítl</string> + <string name="cordn_create_outcome_link">Poslat odkaz</string> + <string name="cordn_create_outcome_no_welcome">Přidán do skupiny, ale pozvánka ho nezastihla — zkuste to znovu z obrazovky skupiny</string> + <string name="cordn_create_outcome_open">Otevřít chat</string> + <string name="cordn_create_outcome_skipped">Zde není žádný klíč, takže nešlo ponechat zprávu Welcome</string> + <string name="cordn_create_outcome_title">Skupina existuje</string> + <string name="cordn_create_outcome_waiting">Čeká se na ně</string> + <string name="cordn_create_people_done">Hotovo</string> + <string name="cordn_create_people_empty">Zatím nikdo nepřidán. Skupinu lze vytvořit prázdnou a pozvánky poslat později, ale právě uvedením lidí teď zjistíte, který koordinátor je zastihne.</string> + <string name="cordn_create_people_none">Zatím nikdo — klepnutím přidáte lidi</string> + <string name="cordn_create_people_note">Uvedením lidí teď zjistíme, který koordinátor je zastihne. Každý z nich dostane zprávu Welcome ponechanou na tomto koordinátoru a vyzvedne si ji, až příště otevře svou aplikaci.</string> + <plurals name="cordn_create_reach_count"> + <item quantity="one">Dostupný na %1$d koordinátoru</item> + <item quantity="few">Dostupný na %1$d koordinátorech</item> + <item quantity="many">Dostupný na %1$d koordinátoru</item> + <item quantity="other">Dostupný na %1$d koordinátorech</item> + </plurals> + <string name="cordn_create_reach_none">Žádný klíč na žádném z vašich koordinátorů</string> + <string name="cordn_create_reach_unknown">Zatím neověřeno</string> + <string name="cordn_create_step_admins">Kdo může přidávat a odebírat</string> + <string name="cordn_create_step_name">Pojmenování</string> + <string name="cordn_create_step_people">Kdo v ní bude</string> + <string name="cordn_create_step_where">Kde poběží</string> + <string name="cordn_create_title">Nová skupina Cordn</string> + <plurals name="cordn_create_unreachable_count"> + <item quantity="one">%1$d osobu nezastihne žádný z vašich koordinátorů</item> + <item quantity="few">%1$d osoby nezastihne žádný z vašich koordinátorů</item> + <item quantity="many">%1$d osoby nezastihne žádný z vašich koordinátorů</item> + <item quantity="other">%1$d osob nezastihne žádný z vašich koordinátorů</item> + </plurals> + <string name="cordn_create_you">Vy, vždy administrátor</string> + <string name="cordn_delete_confirm_body">Všichni ve skupině uvidí tuto zprávu jako staženou a Amethyst přestane zobrazovat její text. Koordinátor si stále uchovává zašifrovanou kopii, v jaké byla odeslána, a kdo ji už četl, ten ji už četl.</string> + <string name="cordn_delete_confirm_title">Stáhnout zprávu</string> + <string name="cordn_delivery_accepted">Přijato koordinátorem</string> + <string name="cordn_exposure_close">Rozumím</string> + <string name="cordn_exposure_open">Co tento koordinátor vidí</string> + <string name="cordn_group_info">Informace o skupině</string> + <string name="cordn_group_unavailable">Tato skupina není dostupná. Její koordinátor mohl být odebrán.</string> + <string name="cordn_group_unavailable_detail">Přidejte koordinátora v Nastavení, abyste mohli začít používat skupiny cordn.</string> + <plurals name="cordn_groups_count"> + <item quantity="one">%1$d skupina</item> + <item quantity="few">%1$d skupiny</item> + <item quantity="many">%1$d skupiny</item> + <item quantity="other">%1$d skupin</item> + </plurals> + <string name="cordn_groups_down_note">Z těchto skupin nic nepřichází. Amethyst to stále zkouší znovu.</string> + <string name="cordn_groups_manage">Koordinátoři</string> + <string name="cordn_groups_none">Zatím žádné skupiny cordn</string> + <string name="cordn_groups_none_detail">Skupina cordn žije na jednom koordinátoru, který spravuje její členství a předává zprávy, aniž by kdy viděl, co kdo píše.</string> + <string name="cordn_groups_start">Založit skupinu</string> + <string name="cordn_info_add_member">Přidat někoho</string> + <string name="cordn_info_add_member_none">Pod tímto jménem nikdo nenalezen.</string> + <string name="cordn_info_add_member_note">Přidat lze jen ty, kdo na tomto koordinátoru zveřejnili balíček klíčů.</string> + <string name="cordn_info_add_member_placeholder">Jméno, npub nebo jmeno@domena</string> + <string name="cordn_info_admin_badge">Admin</string> + <plurals name="cordn_info_admins_kept"> + <item quantity="one">%1$d administrátor skupiny zůstává beze změny.</item> + <item quantity="few">%1$d administrátoři skupiny zůstávají beze změny.</item> + <item quantity="many">%1$d administrátora skupiny zůstává beze změny.</item> + <item quantity="other">%1$d administrátorů skupiny zůstává beze změny.</item> + </plurals> + <string name="cordn_info_coordinator">Koordinátor</string> + <string name="cordn_info_coordinator_key">Klíč koordinátora</string> + <string name="cordn_info_coordinator_nprofile">Odkaz na koordinátora</string> + <string name="cordn_info_copy_nprofile">Kopírovat nprofile koordinátora</string> + <string name="cordn_info_edit_details">Upravit údaje</string> + <string name="cordn_info_egalitarian">Tato skupina nemá administrátory: každý může přidávat, odebírat a přejmenovávat, a to natrvalo.</string> + <string name="cordn_info_epoch">Epocha</string> + <string name="cordn_info_gid">ID skupiny</string> + <string name="cordn_info_has_key_package">Lze přidat na tomto koordinátoru</string> + <string name="cordn_info_members">Členové</string> + <string name="cordn_info_remove_confirm_body">%1$s odsud přestane dostávat zprávy. Co už přečetl, mu zůstane, a nelze to vrátit zpět — musel by být pozván znovu.</string> + <string name="cordn_info_remove_confirm_title">Odebrat ze skupiny?</string> + <string name="cordn_info_remove_member">Odebrat</string> + <string name="cordn_info_save">Uložit</string> + <string name="cordn_info_technical">Technické podrobnosti</string> + <string name="cordn_invitations_accept">Připojit se</string> + <string name="cordn_invitations_decline">Odmítnout</string> + <string name="cordn_invitations_decline_warning">Odmítnutí je trvalé. Pro návrat budete potřebovat novou pozvánku.</string> + <string name="cordn_invitations_explainer">Kontroluje se jen při otevření této obrazovky a nikdy jindy — dotaz na koordinátora, zda vás někdo pozval, mu prozradí, že jste tady, a proto to Amethyst nedělá na pozadí.</string> + <string name="cordn_invitations_load_failed">Pozvánky se nepodařilo načíst.</string> + <plurals name="cordn_invitations_members"> + <item quantity="one">%1$d osoba už je v této skupině</item> + <item quantity="few">%1$d osoby už jsou v této skupině</item> + <item quantity="many">%1$d osoby už je v této skupině</item> + <item quantity="other">%1$d osob už je v této skupině</item> + </plurals> + <string name="cordn_invitations_members_more">+%1$d další</string> + <string name="cordn_invitations_no_coordinators">Nemáte otevřeného žádného koordinátora. Pozvánky cordn přicházejí přes koordinátora, takže zatím není kde hledat.</string> + <string name="cordn_invitations_none">Žádné čekající pozvánky.</string> + <string name="cordn_invitations_refresh">Zkontrolovat znovu</string> + <string name="cordn_invitations_skipped">Pozvánka byla ponechána pro jiné zařízení</string> + <string name="cordn_invitations_title">Pozvánky Cordn</string> + <string name="cordn_invitations_unreachable">%1$s neodpověděl</string> + <string name="cordn_invitations_via">Přes %1$s</string> + <string name="cordn_keypackages_disclosure">Zveřejněním podepíšete záznam vlastním klíčem svého účtu. Daný koordinátor se pak dozví, že tento účet existuje a lze ho pozvat — natrvalo, a bez ohledu na to, zda vás někdo pozve.</string> + <string name="cordn_keypackages_explainer">Ostatní vás mohou přidat do skupiny cordn jen pomocí balíčku klíčů, který jste zveřejnili na daném koordinátoru. Cordn ho nemá kde jinde uchovat, takže bez zveřejněného balíčku vás nikdo nemůže pozvat — a nic mu neřekne proč.</string> + <string name="cordn_keypackages_failed">Balíčky klíčů se nepodařilo načíst.</string> + <string name="cordn_keypackages_last_resort_no">Žádný záložní balíček. Jakmile dojdou jednorázové, pozvánky budou selhávat.</string> + <string name="cordn_keypackages_last_resort_yes">Záložní balíček je zveřejněn, takže pozvánku lze vytvořit i poté, co dojdou jednorázové.</string> + <plurals name="cordn_keypackages_orphans"> + <item quantity="one">%1$d balíček, který toto zařízení nedokáže otevřít</item> + <item quantity="few">%1$d balíčky, které toto zařízení nedokáže otevřít</item> + <item quantity="many">%1$d balíčku, které toto zařízení nedokáže otevřít</item> + <item quantity="other">%1$d balíčků, které toto zařízení nedokáže otevřít</item> + </plurals> + <string name="cordn_keypackages_orphans_body">Koordinátor je předá každému, kdo vás zve, ale soukromá část není na tomto zařízení — patří jiné instalaci. Pokud tato instalace už neexistuje, pozvánka s jedním z nich vytvoří zprávu Welcome, kterou nikdo nikdy neotevře.</string> + <string name="cordn_keypackages_publish">Zveřejnit jeden</string> + <string name="cordn_keypackages_publish_last_resort">Zveřejnit záložní</string> + <plurals name="cordn_keypackages_summary"> + <item quantity="one">K dispozici je %1$d jednorázový balíček.</item> + <item quantity="few">K dispozici jsou %1$d jednorázové balíčky.</item> + <item quantity="many">K dispozici je %1$d jednorázového balíčku.</item> + <item quantity="other">K dispozici je %1$d jednorázových balíčků.</item> + </plurals> + <string name="cordn_keypackages_topup_note">Jakmile zde poprvé něco zveřejníte, Amethyst bude spotřebované balíčky potichu nahrazovat. Ten první ale nikdy nezveřejní za vás.</string> + <string name="cordn_keypackages_withdraw_all">Stáhnout vše</string> + <string name="cordn_keypackages_withdraw_orphans">Stáhnout tyto</string> + <string name="cordn_link_request">Požádat o připojení k této skupině</string> + <string name="cordn_link_request_disclosure">Tímto zveřejníte balíček klíčů pod vlastním klíčem svého účtu a sdělíte koordinátorovi, že se chcete připojit k této skupině — bez ohledu na to, zda někdo odpoví.</string> + <string name="cordn_link_request_failed">O připojení se nepodařilo požádat.</string> + <string name="cordn_link_request_sent">Požádáno. Některý člen skupiny vás musí přidat; pozvánka se pak objeví mezi pozvánkami cordn.</string> + <string name="cordn_link_scan">Skenovat</string> + <string name="cordn_media_download_failed">Tento soubor se nepodařilo otevřít.</string> + <string name="cordn_media_no_server">Pro tento účet není nastaven žádný mediální server, takže soubor nemá kam uložit. Vyberte ho v Nastavení.</string> + <string name="cordn_media_unreadable">Tento soubor nelze přečíst.</string> + <string name="cordn_media_upload_failed">Tento soubor se nepodařilo odeslat.</string> + <plurals name="cordn_member_count"> + <item quantity="one">%1$d člen</item> + <item quantity="few">%1$d členové</item> + <item quantity="many">%1$d člena</item> + <item quantity="other">%1$d členů</item> + </plurals> + <string name="cordn_message_deleted">Zpráva smazána</string> + <string name="cordn_message_details_coordinator">Koordinátor</string> + <string name="cordn_message_details_cursor">Kurzor</string> + <string name="cordn_message_details_sent_at">Odesláno</string> + <string name="cordn_message_details_title">Podrobnosti zprávy</string> + <string name="cordn_message_edited">upraveno</string> + <string name="cordn_pinned_by">Připnul %1$s</string> + <plurals name="cordn_pinned_count"> + <item quantity="one">%1$d připnutá zpráva v této skupině</item> + <item quantity="few">%1$d připnuté zprávy v této skupině</item> + <item quantity="many">%1$d připnuté zprávy v této skupině</item> + <item quantity="other">%1$d připnutých zpráv v této skupině</item> + </plurals> + <string name="cordn_pinned_next">Další připnutá zpráva</string> + <string name="cordn_pinned_previous">Předchozí připnutá zpráva</string> + <string name="cordn_pinned_show_all">Zobrazit všechny připnuté zprávy</string> + <string name="cordn_pinned_title">Připnuté zprávy</string> + <string name="cordn_reactions_title">Reakce</string> + <string name="cordn_requests_accept">Přidat</string> + <string name="cordn_requests_admin_only">Přijmout může jen administrátor. Ve skupině bez administrátorů to je kdokoli.</string> + <string name="cordn_requests_check">Zkontrolovat žádosti</string> + <string name="cordn_requests_decline">Zamítnout</string> + <string name="cordn_requests_failed">Žádosti se nepodařilo načíst.</string> + <string name="cordn_requests_none">Nikdo nečeká na připojení.</string> + <string name="cordn_requests_title">Žádosti o připojení</string> + <string name="cordn_send">Odeslat</string> + <string name="cordn_send_failed">Odeslání se nezdařilo.</string> + <string name="cordn_send_no_session">Koordinátor této skupiny není otevřen, takže zatím nelze nic odeslat.</string> + <string name="cordn_share_copied">Zkopírováno</string> + <string name="cordn_share_copy">Kopírovat odkaz</string> + <string name="cordn_share_explainer">Kdokoli s tímto odkazem může požádat o připojení. Pustit ho dovnitř ale musí administrátor.</string> + <string name="cordn_share_title">Sdílet tuto skupinu</string> + <string name="cordn_voice_play">Přehrát hlasovou zprávu</string> + <string name="cordn_voice_record">Nahrát hlasovou zprávu</string> + <string name="cordn_voice_stop">Zastavit a odeslat</string> + <string name="qr_scanner_camera_rationale">Amethyst potřebuje fotoaparát ke skenování QR kódů. Nic se nenahrává ani neodesílá — snímky se dekódují v zařízení a poté se zahodí.</string> + <string name="qr_scanner_grant_camera">Povolit fotoaparát</string> + <string name="qr_scanner_camera_blocked">Přístup k fotoaparátu je pro Amethyst vypnutý. Znovu ho můžete zapnout v nastavení Androidu.</string> + <string name="qr_scanner_open_settings">Otevřít nastavení</string> + <string name="qr_scanner_unavailable">Dekodér QR kódů se v tomto zařízení nepodařilo spustit. Kód můžete stále vložit jako text.</string> + <string name="qr_scanner_torch_on">Zapnout světlo</string> + <string name="qr_scanner_torch_off">Vypnout světlo</string> + <string name="qr_scanner_dark_hint">Je tma — zkuste světlo</string> + <string name="qr_scanner_scan_image">Naskenovat obrázek</string> + <string name="qr_scanner_paste">Vložit</string> + <string name="qr_scanner_no_code_in_image">V obrázku nebyl nalezen žádný QR kód</string> + <string name="qr_scanner_clipboard_empty">Ve schránce není nic ke skenování</string> + <string name="qr_scanner_pick_one">Klepněte na požadovaný kód</string> + <plurals name="qr_scanner_sequence_progress"> + <item quantity="one">Načteno %1$s z %2$s části — pokračujte</item> + <item quantity="few">Načteno %1$s z %2$s částí — pokračujte</item> + <item quantity="many">Načteno %1$s z %2$s části — pokračujte</item> + <item quantity="other">Načteno %1$s z %2$s částí — pokračujte</item> + </plurals> + <string name="qr_scanner_zoom_reset">Obnovit přiblížení</string> + <string name="qr_scanner_unsupported_title">Amethyst tento kód nedokáže otevřít</string> + <string name="qr_scanner_unsupported_secret">Tento kód obsahuje soukromý klíč nebo tajemství pro spárování, proto se jeho obsah zde nezobrazuje. Použijte obrazovku, která ho očekává.</string> + <string name="qr_scanner_kind_nsec">Toto je soukromý klíč. Vložte ho raději na přihlašovací obrazovce.</string> + <string name="qr_scanner_kind_wallet">Toto je připojení peněženky. Přidejte ho v nastavení peněženky.</string> + <string name="qr_scanner_kind_bunker">Toto je adresa vzdáleného podepisovatele. Amethyst je rozdává, aby ho ostatní aplikace mohly žádat o podpis — přihlásit se s ní nedokáže.</string> + <string name="qr_scanner_kind_signer">Toto je aplikace, která se chce připojit k vašemu podepisovateli. Otevřete ji z obrazovky podepisovatele NIP-46.</string> + <string name="qr_scanner_kind_lightning">Toto je Lightning faktura.</string> + <string name="qr_scanner_kind_cashu">Toto je Cashu token.</string> + <string name="qr_scanner_kind_web">Toto je webový odkaz.</string> + <string name="qr_scanner_kind_text">Toto je prostý text, ne kód Nostr.</string> + <string name="qr_scanner_kind_nostr_unsupported">Toto je kód Nostr, ale tato obrazovka ho nedokáže otevřít.</string> + <string name="qr_scanner_label_profile">Profil</string> + <string name="qr_scanner_label_note">Poznámka</string> + <string name="qr_scanner_label_nostr">Kód Nostr</string> + <string name="qr_scanner_label_relay">Relé</string> + <string name="qr_scanner_label_nsec">Soukromý klíč</string> + <string name="qr_scanner_label_wallet">Připojení peněženky</string> + <string name="qr_scanner_label_bunker">Adresa vzdáleného podepisovatele</string> + <string name="qr_scanner_label_signer">Žádost o připojení k podepisovateli</string> + <string name="qr_scanner_label_lightning">Lightning faktura</string> + <string name="qr_scanner_label_cashu">Cashu token</string> + <string name="qr_scanner_label_web">Webový odkaz</string> + <string name="qr_scanner_label_text">Prostý text</string> + <string name="qr_scanner_open_link">Otevřít odkaz</string> + <string name="qr_scanner_copy">Kopírovat</string> + <string name="qr_scanner_copied">Zkopírováno</string> + <string name="qr_scanner_try_again">Skenovat znovu</string> + <string name="workout_suggestion_connect_title">Využijte tréninky, které už máte zaznamenané</string> + <string name="workout_suggestion_connect_message">Připojte Health Connect (Samsung Health, Google Fit, Fitbit, Garmin…) — získáte Moji kondici, vlastní přehled tréninků, a tento trénink vyplníte podle záznamu z hodinek místo ručního psaní každého čísla.</string> + <string name="health_connect_rationale_headline">Amethyst používá Health Connect pro funkci Moje kondice — váš vlastní přehled tréninků přímo v telefonu.</string> + <string name="health_connect_rationale_intro">Moje kondice (menu → Vy → Moje kondice) ukazuje, jak trénujete: kolik jste toho udělali tento týden oproti minulému, jak se váš čas dělí mezi běh, cyklistiku, chůzi, plavání a posilovnu, vaše nejlepší výkony, kolik dní jste trénovali a vaši aktuální sérii. Amethyst tento obraz sestaví z tréninků, které vaše hodinky nebo fitness aplikace už uložily do Health Connect, spolu s tréninky, které jste zaznamenali přímo v Amethystu. Vše se vypočítá ve vašem telefonu a zobrazí jen vám.</string> + <string name="health_connect_rationale_exercise">Cvičení · jakou aktivitu jste dělali, kdy začala a jak dlouho trvala. To je samotný trénink: určuje počet tréninků, dobu tréninku, rozpis podle aktivit, aktivní dny a vaši sérii.</string> + <string name="health_connect_rationale_distance">Vzdálenost · jak daleko jste se dostali; zobrazuje se jako vzdálenost tento týden oproti minulému, týdenní průměr, vzdálenost podle aktivity a nejdelší vzdálenost.</string> + <string name="health_connect_rationale_calories">Aktivní a celkové kalorie · energie spálená při tréninku; zobrazuje se jako energie tento týden a její srovnání s minulým týdnem. Aktivní kalorie se použijí, pokud je váš zdroj zaznamenává; celkové kalorie slouží jako náhrada u zdrojů, které zaznamenávají jen celkovou energii.</string> + <string name="health_connect_rationale_heart_rate">Tepová frekvence · průměr a maximum během každého tréninku, sloučené do průměrné a nejvyšší tepové frekvence za období. Podle ní poznáte lehký týden od náročného při stejné vzdálenosti.</string> + <string name="health_connect_rationale_steps">Kroky · počet kroků při běhu, chůzi nebo túře; zobrazuje se jako týdenní průměr kroků a nejvyšší počet kroků.</string> + <string name="health_connect_rationale_elevation">Nastoupané převýšení · kolik jste nastoupali; zobrazuje se jako týdenní průměr stoupání a největší stoupání. Právě to odlišuje rovinatý týden od kopcovitého.</string> + <string name="health_connect_rationale_limit_window">Čte jen tréninky z posledních 4 týdnů, a to jen když je otevřená Moje kondice nebo editor tréninku. Nikdy nečte na pozadí.</string> + <string name="health_connect_rationale_limit_publish">Vaše zdravotní údaje nikdy nikam neposílá. Moje kondice se počítá ve vašem telefonu a zůstává v něm. Pokud později budete chtít zveřejnit jeden trénink, klepnete u něj na Sdílet, přečtete si příspěvek, který Amethyst vyplnil, a zveřejníte ho sami — vždy jen jeden trénink, nikdy automaticky.</string> + <string name="health_connect_rationale_limit_optional">Celá funkce je volitelná. Oprávnění můžete kdykoli odvolat v Health Connect — Moje kondice se pak vrátí k úvodní výzvě — nebo vypnout návrhy v editoru v Nastavení → Nastavení editoru. Zbytek Amethystu funguje dál.</string> + <string name="my_fitness_title">Moje kondice</string> + <string name="my_fitness_window">Poslední 4 týdny</string> + <string name="my_fitness_loading_metrics">Načítám vzdálenost, kalorie a tepovou frekvenci…</string> + <string name="my_fitness_window_note">Poslední čtyři celé týdny vašeho tréninku: tréninky, které jste zveřejnili, a vše, co Health Connect zaznamenává v tomto telefonu. Amethyst přehled počítá v zařízení a nic z něj nezveřejňuje.</string> + <string name="my_fitness_this_week">Tento týden</string> + <string name="my_fitness_vs_last_week">oproti minulému týdnu</string> + <string name="my_fitness_weekly_average">Týdenní průměr</string> + <string name="my_fitness_workouts">Tréninky</string> + <string name="my_fitness_active_days">Aktivní dny</string> + <string name="my_fitness_streak">Série dní</string> + <string name="my_fitness_time">Čas</string> + <string name="my_fitness_duration_hours_minutes">%1$d h %2$d min</string> + <string name="my_fitness_duration_minutes">%1$d min</string> + <string name="my_fitness_duration_seconds">%1$d s</string> + <string name="my_fitness_distance">Vzdálenost</string> + <string name="my_fitness_calories">Kalorie</string> + <string name="my_fitness_steps">Kroky</string> + <string name="my_fitness_elevation">Stoupání</string> + <string name="my_fitness_avg_heart_rate">Prům. TF</string> + <string name="my_fitness_max_heart_rate">Max. TF</string> + <string name="my_fitness_by_activity">Podle aktivity</string> + <string name="my_fitness_bests">Nejlepší výkony</string> + <string name="my_fitness_best_longest_distance">Nejdelší vzdálenost</string> + <string name="my_fitness_best_longest_duration">Nejdelší trénink</string> + <string name="my_fitness_best_biggest_climb">Největší stoupání</string> + <string name="my_fitness_best_most_steps">Nejvíce kroků</string> + <string name="my_fitness_best_highest_heart_rate">Nejvyšší tepová frekvence</string> + <string name="my_fitness_recent">Nedávné tréninky</string> + <string name="my_fitness_empty">Za poslední 4 týdny nic zaznamenáno. Zobrazí se zde tréninky, které zveřejníte, i ty, které zaznamenají vaše hodinky.</string> + <string name="my_fitness_connect_title">Sledujte svůj trénink</string> + <string name="my_fitness_connect_message">Připojte Health Connect a uvidíte týdenní součty, vývoj oproti minulému týdnu, nejlepší výkony a tréninkovou sérii. Vše zůstává ve vašem telefonu.</string> + <string name="my_fitness_connect_banner">Připojte Health Connect a přidejte tréninky zaznamenané hodinkami, včetně tepové frekvence, kroků a stoupání.</string> + <string name="my_fitness_connect_button">Připojit</string> + <string name="my_fitness_share">Sdílet tento trénink</string> + <string name="my_fitness_unit_bpm">tep/min</string> + <string name="my_fitness_unit_kcal">kcal</string> + <string name="my_fitness_unit_km">km</string> + <string name="my_fitness_unit_mi">mi</string> + <string name="my_fitness_unit_m">m</string> + <string name="my_fitness_unit_ft">ft</string> + <string name="looking_for_event_title">Načítání</string> + <string name="looking_for_private_event">Čekám na načtení vašich soukromých zpráv…</string> + <string name="call_duration_minutes_seconds">%1$02d:%2$02d</string> + <string name="call_duration_hours_minutes_seconds">%1$d:%2$02d:%3$02d</string> + <string name="marmot_preview_group_updated">Skupina aktualizována</string> + <string name="marmot_preview_media">Příloha</string> + <string name="marmot_preview_no_text">Zpráva</string> + <string name="marmot_preview_with_sender">%1$s: %2$s</string> + <string name="marmot_empty_create_action">Vytvořit skupinu</string> + <string name="marmot_invite_device_banner">Pozvánky Marmot chodí na jiné zařízení a zde je nelze otevřít.</string> + <string name="marmot_invite_device_banner_dismiss">Zavřít</string> + <string name="marmot_invite_device_publish">Zveřejnit z tohoto zařízení</string> + <string name="wallet_open_zapped_note">Otevřít poznámku se zapem</string> + <string name="video_player_settings_action_captions">Titulky</string> + <string name="video_player_settings_action_captions_description">Zapnout nebo vypnout titulky (skryto, pokud je video nemá)</string> + <string name="app_notification_reply_failed">Neodesláno</string> + <string name="app_notification_reply_retry">Zkusit znovu</string> + <string name="app_notification_reply_sending">Odesílání…</string> + <string name="app_notification_reply_unconfirmed">Stále se odesílá…</string> + <string name="app_notification_shortcut_account_removed">Tento účet byl z Amethystu odebrán.</string> + <string name="captions_off">Vypnuto</string> + <string name="captions_turn_off">Vypnout titulky</string> + <string name="captions_turn_on">Zapnout titulky</string> + <string name="captions_unnamed_track">Titulky</string> + <string name="kind_app_curation_set">Seznam aplikací</string> + <string name="kind_article_curation_set">Seznam článků</string> + <string name="kind_bookmark_set">Sada záložek</string> + <string name="kind_favorite_algo_feeds">Oblíbené kanály</string> + <string name="kind_git_authors">Git autoři</string> + <string name="kind_git_repositories">Git repozitáře</string> + <string name="kind_good_wiki_authors">Důvěryhodní autoři wiki</string> + <string name="kind_good_wiki_relays">Důvěryhodná wiki relé</string> + <string name="kind_interest_set">Zájmy</string> + <string name="kind_media_follows">Sledování médií</string> + <string name="kind_media_starter_pack">Startovní balíček médií</string> + <string name="kind_mute_set_for_kind">Ztlumeno pro druh %1$s</string> + <string name="kind_picture_curation_set">Seznam obrázků</string> + <string name="kind_simple_groups">Skupiny</string> + <string name="kind_video_collaboration">Spolupráce na videu</string> + <string name="kind_video_list">Seznam videí</string> + <string name="kind_video_subtitles">Titulky videa</string> + <string name="login_bunker_not_supported">Amethyst pro Android se nemůže přihlásit pomocí adresy bunker://. Tyto adresy rozdává, aby ho ostatní aplikace mohly žádat o podpis, ale přihlásit se s nimi nedokáže. Použijte externí podepisovací aplikaci nebo Amethyst Desktop.</string> + <string name="login_nostrconnect_not_supported">Toto je aplikace, která se chce připojit k vašemu podepisovateli, ne způsob přihlášení. Nejprve se přihlaste a pak ji otevřete z obrazovky podepisovatele NIP-46.</string> + <string name="my_fitness">Moje kondice</string> + <string name="nip51_list_has_private_members">Tento seznam má soukromé členy, které vidí jen jeho autor</string> + <string name="route_edit_geocache">Upravit keš</string> + <string name="route_edit_geocache_hunt">Upravit výpravu</string> + <string name="route_geocache_detail">Keš</string> + <string name="route_geocache_hunt_detail">Výprava</string> + <string name="route_geocache_hunts">Výpravy</string> + <string name="route_geocaches">Keše</string> + <string name="route_log_geocache_find">Zapsat nález</string> + <string name="route_new_geocache">Ukrýt keš</string> + <string name="route_new_geocache_hunt">Nová výprava</string> + <string name="video_collaboration_accepted_on">Přijal uvedení jako spolupracovník</string> + <string name="video_collaboration_declined_on">Odmítl uvedení jako spolupracovník</string> + <string name="video_credit_accepted">Uvedení přijato</string> + <string name="video_credit_audio_from">Zvuk od</string> + <string name="video_credit_featuring">Účinkuje</string> + <string name="video_credit_inspired_by">Inspirováno</string> + <string name="video_credit_references">Odkazuje na</string> + <string name="video_curation_set_private_items">Soukromý seznam</string> + <plurals name="video_curation_set_video_count"> + <item quantity="one">%1$d video</item> + <item quantity="few">%1$d videa</item> + <item quantity="many">%1$d videa</item> + <item quantity="other">%1$d videí</item> + </plurals> + <string name="marmot_invite_device_success">Toto zařízení nyní přijímá vaše pozvánky Marmot.</string> + <string name="marmot_invite_device_failure">Zveřejnění KeyPackage selhalo: %1$s</string> + <string name="marmot_invite_device_rejected">Žádné relé KeyPackage nepřijalo. Zkontrolujte nastavení relé a zkuste to znovu.</string> + <string name="chat_delivery_sending">Odesílání…</string> + <string name="chat_delivery_failed">Nepodařilo se odeslat. Klepnutím to zkusíte znovu.</string> + <plurals name="marmot_unread_messages"> + <item quantity="one">%1$d nová zpráva</item> + <item quantity="few">%1$d nové zprávy</item> + <item quantity="many">%1$d nové zprávy</item> + <item quantity="other">%1$d nových zpráv</item> + </plurals> + <string name="chat_type_cordn_title">Skupiny Cordn</string> + <string name="chat_type_cordn_desc">End-to-end šifrované skupiny, jejichž pořadí zpráv určuje vámi zvolený koordinátor, nikoli relé.</string> + <string name="new_conversation_cordn_title">Skupina Cordn</string> + <string name="new_conversation_cordn_tagline">Šifrovaný skupinový chat řazený koordinátorem, kterého si vyberete.</string> + <string name="new_conversation_cordn_chip">Koordinovaná</string> + <string name="new_conversation_cordn_best">Nejlepší pro týmy, které chtějí jedno spolehlivé pořadí konverzace.</string> + <string name="new_conversation_cordn_cta">Vytvořit skupinu Cordn</string> + <string name="new_conversation_cordn_pro_1">End-to-end šifrování pomocí MLS — koordinátor nikdy nepřečte žádnou zprávu.</string> + <string name="new_conversation_cordn_pro_2">Jedno dohodnuté pořadí pro všechny, takže historii nemohou přeházet špatně nastavené hodiny.</string> + <string name="new_conversation_cordn_con_1">Koordinátor zjistí, kdo je ve skupině a kdy spolu mluvíte, i když nemůže číst, co píšete.</string> + <string name="sno_object_title">3D objekt</string> + <string name="sno_object_details">%1$d vrcholů · %2$d stěn</string> + <string name="sno_object_unreadable">Tento 3D objekt nelze přečíst (pravidlo %1$s)</string> + <string name="sno_avatar_title">Avatar Cyberspace</string> + <string name="sno_avatar_unpaid">Tento avatar nezaplatil za svou velikost, proto se nevykresluje</string> + <string name="sno_light_on">Osvětleno; klepnutím vykreslíte objekt plošně</string> + <string name="sno_light_off">Plošně; klepnutím objekt osvětlíte</string> + <string name="sno_object_scale">1 jednotka = %1$s</string> + <string name="sno_shard_dataspace">Ukryto v dataspace · %1$s</string> + <string name="sno_shard_ideaspace">Ukryto v ideaspace · %1$s</string> + <string name="sno_avatar_default">Výchozí avatar</string> + <string name="sno_avatar_default_details">Není zveřejněn žádný tvar, takže všichni vidí tento</string> + <string name="cyberspace_bag_title">Ukryto na místě</string> + <string name="cyberspace_bag_box">Ukryto v bloku %1$s regionů</string> + <string name="cyberspace_bag_destination">Ukryto v jednom regionu, který nápověda přesně určuje</string> + <string name="cyberspace_bag_no_hint">Bez nápovědy, takže to může být kdekoli v kyberprostoru</string> + <string name="cyberspace_bag_out_of_reach">Mimo dosah: nápověda určuje 2^%1$d regionů k prohledání</string> + <string name="cyberspace_bag_search">Hledat</string> + <string name="cyberspace_bag_cancel">Zastavit</string> + <string name="cyberspace_bag_measuring">Odhaduji náročnost hledání v tomto zařízení…</string> + <string name="cyberspace_bag_progress">Prohledáno %1$s z %2$s regionů</string> + <plurals name="cyberspace_bag_too_slow_minutes"> + <item quantity="one">Mimo dosah: zhruba %1$d minuta hledání v tomto zařízení</item> + <item quantity="few">Mimo dosah: zhruba %1$d minuty hledání v tomto zařízení</item> + <item quantity="many">Mimo dosah: zhruba %1$d minuty hledání v tomto zařízení</item> + <item quantity="other">Mimo dosah: zhruba %1$d minut hledání v tomto zařízení</item> + </plurals> + <plurals name="cyberspace_bag_too_slow_hours"> + <item quantity="one">Mimo dosah: zhruba %1$d hodina hledání v tomto zařízení</item> + <item quantity="few">Mimo dosah: zhruba %1$d hodiny hledání v tomto zařízení</item> + <item quantity="many">Mimo dosah: zhruba %1$d hodiny hledání v tomto zařízení</item> + <item quantity="other">Mimo dosah: zhruba %1$d hodin hledání v tomto zařízení</item> + </plurals> + <string name="cyberspace_bag_not_found">Není v bloku, který nápověda určila</string> + <string name="cyberspace_bag_damaged">Region nalezen, ale obsah nelze přečíst</string> + <plurals name="cyberspace_bag_dropped"> + <item quantity="one">%1$d položka ukrytá zde neodpovídala svému podpisu a byla zahozena</item> + <item quantity="few">%1$d položky ukryté zde neodpovídaly svým podpisům a byly zahozeny</item> + <item quantity="many">%1$d položky ukryté zde neodpovídalo svým podpisům a bylo zahozeno</item> + <item quantity="other">%1$d položek ukrytých zde neodpovídalo svým podpisům a bylo zahozeno</item> + </plurals> + <string name="cyberspace_bag_empty">Otevřeno, ale uvnitř nic nebylo</string> + <string name="cyberspace_bag_unsigned">Nepodepsáno, takže autorství je jen tvrzení</string> + <string name="cyberspace_bag_unknown_kind">Položka druhu %1$d, kterou tento klient nevykresluje</string> + <plurals name="cyberspace_bag_opaque"> + <item quantity="one">%1$d bajt něčeho, co tento klient neumí přečíst</item> + <item quantity="few">%1$d bajty něčeho, co tento klient neumí přečíst</item> + <item quantity="many">%1$d bajtu něčeho, co tento klient neumí přečíst</item> + <item quantity="other">%1$d bajtů něčeho, co tento klient neumí přečíst</item> + </plurals> + <string name="browser_pill_close">Zavřít</string> + <string name="browser_pill_tile_copy">Kopírovat odkaz</string> + <string name="browser_pill_tile_find">Najít</string> + <string name="browser_pill_tile_text">Velikost textu</string> + <string name="browser_pill_tile_desktop">Počítačová verze</string> + <string name="browser_pill_tile_home">Na plochu</string> + <string name="browser_pill_tile_other">Jiný prohlížeč</string> + <string name="browser_pill_tile_full">Celá obrazovka</string> + <string name="browser_pill_edit_address">Upravit adresu</string> + <string name="browser_pill_back">Zpět</string> + <string name="browser_pill_forward">Vpřed</string> + <string name="browser_pill_reload">Obnovit</string> + <string name="browser_pill_stop">Zastavit načítání</string> + <string name="browser_pill_favorite_add">Přidat do oblíbených</string> + <string name="browser_pill_favorite_remove">Odebrat z oblíbených</string> + <string name="browser_pill_share">Sdílet</string> + <string name="browser_pill_copy">Kopírovat odkaz</string> + <string name="browser_pill_find">Najít na stránce</string> + <string name="browser_pill_text_size">Velikost textu</string> + <string name="browser_pill_add_home">Přidat na plochu</string> + <string name="browser_pill_desktop">Verze pro počítač</string> + <string name="browser_pill_other_browser">Otevřít v prohlížeči</string> + <string name="browser_pill_other_browser_named">Otevřít v aplikaci %1$s</string> + <string name="browser_pill_full_screen">Celá obrazovka</string> + <string name="browser_pill_left_site">Opustili jste %1$s</string> + <string name="browser_pill_back_to_app">Zpět do aplikace</string> + <string name="browser_pill_text_smaller">Menší text</string> + <string name="browser_pill_text_larger">Větší text</string> + <string name="browser_pill_text_reset">Obnovit</string> + <string name="browser_pill_text_value">%1$d%</string> + <string name="browser_pill_privacy">Soukromí</string> + <string name="browser_pill_tor_title">Onion routing</string> + <string name="browser_pill_tor_on">Web nevidí vaši IP adresu</string> + <string name="browser_pill_tor_off">Web vidí vaši IP adresu</string> + <string name="browser_pill_site_settings">Nastavení webu</string> + <string name="browser_pill_site_settings_none">Zatím nic nepovoleno</string> + <string name="browser_pill_access">K čemu má přístup</string> + <string name="browser_pill_access_desc">Vaše klíče nikdy neopustí Amethyst</string> + <string name="browser_pill_access_keys_desc">Každé podepsání, zveřejnění, nahrání nebo platba vyžaduje vaše schválení</string> + <string name="browser_pill_access_none">Žádný zvláštní přístup</string> + <string name="browser_pill_access_none_desc">Běží v izolovaném prostředí a nemůže jednat za váš účet</string> + <string name="browser_pill_access_manage">Spravovat oprávnění</string> + <string name="browser_pill_access_nsite">nSite · izolovaný</string> + <string name="browser_pill_access_napplet">nApplet · izolovaný</string> + <string name="browser_pill_console">Konzole</string> + <string name="browser_pill_security_https">Zabezpečené připojení</string> + <string name="browser_pill_security_http">Nezabezpečeno</string> + <string name="browser_pill_security_tor">Přes onion routing</string> + <string name="browser_pill_security_sandbox">Izolovaná aplikace</string> + <string name="browser_pill_permission_camera">Fotoaparát</string> + <string name="browser_pill_permission_microphone">Mikrofon</string> + <string name="browser_pill_permission_location">Poloha</string> + <string name="browser_pill_decision_ask">Zeptat se</string> + <string name="browser_pill_decision_allow">Povolit</string> + <string name="browser_pill_decision_block">Blokovat</string> + <string name="browser_pill_permission_state">%1$s: %2$s</string> + <string name="browser_pill_decision_allowed">povoleno</string> + <string name="browser_pill_decision_blocked">blokováno</string> + <string name="browser_pill_address_hint">Hledejte nebo zadejte adresu</string> + <string name="browser_pill_go">Přejít</string> + <string name="browser_pill_clear">Vymazat</string> + <string name="browser_pill_paste_go">Vložit a přejít</string> + <string name="browser_pill_fill_in">Použít tuto adresu</string> + <string name="browser_pill_find_hint">Najít na stránce</string> + <string name="browser_pill_find_count">%1$d / %2$d</string> + <string name="browser_pill_find_none">Žádné shody</string> + <string name="browser_pill_find_previous">Předchozí shoda</string> + <string name="browser_pill_find_next">Další shoda</string> + <string name="browser_pill_find_close">Zavřít hledání na stránce</string> + <string name="browser_pill_console_all">Vše</string> + <string name="browser_pill_console_errors">Chyby</string> + <string name="browser_pill_console_warnings">Varování</string> + <string name="browser_pill_console_copy">Kopírovat</string> + <string name="browser_pill_console_clear">Vymazat</string> + <string name="browser_pill_console_empty">Zatím nic nezaznamenáno</string> + <string name="browser_pill_perm_title">Tento web chce použít</string> + <string name="browser_pill_perm_camera_desc">Vidět, co vidí váš fotoaparát</string> + <string name="browser_pill_perm_microphone_desc">Slyšet, co slyší váš mikrofon</string> + <string name="browser_pill_perm_location_desc">Znát vaši přibližnou polohu</string> + <string name="browser_pill_perm_tor_note">Hovory mohou odhalit vaši IP adresu i přes Tor.</string> + <string name="browser_pill_perm_allow">Povolit během návštěvy</string> + <string name="browser_pill_perm_once">Jen tentokrát</string> + <string name="browser_pill_perm_deny">Nepovolit</string> + <string name="browser_pill_dialog_says">%1$s sděluje</string> + <string name="browser_pill_dialog_generic">Tato stránka sděluje</string> + <string name="browser_pill_dialog_block">Nedovolit této stránce zobrazovat další dialogy</string> + <string name="browser_pill_dialog_answer">Vaše odpověď</string> + <string name="browser_pill_dialog_leave_title">Opustit web?</string> + <string name="browser_pill_dialog_leave_message">Provedené změny se nemusí uložit.</string> + <string name="browser_pill_dialog_leave">Opustit</string> + <string name="browser_pill_ok">OK</string> + <string name="browser_pill_cancel">Zrušit</string> + <string name="browser_pill_info_connection">Připojení</string> + <string name="browser_pill_info_https">Připojení je šifrované</string> + <string name="browser_pill_info_https_desc">Informace, které odešlete, nelze cestou přečíst.</string> + <string name="browser_pill_info_http">Připojení není šifrované</string> + <string name="browser_pill_info_http_desc">Nezadávejte hesla ani platební údaje.</string> + <string name="browser_pill_info_tor">Přes onion routing sítí Tor</string> + <string name="browser_pill_info_open">Otevřený web</string> + <string name="browser_pill_info_certificate">Certifikát</string> + <string name="browser_pill_info_certificate_desc">Vydáno pro %1$s, vydavatel %2$s · platí do %3$s</string> + <string name="browser_pill_info_permissions">Oprávnění</string> + <string name="browser_pill_info_site_data">Soubory cookie a data webu</string> + <string name="browser_pill_info_site_data_desc">Přihlášení a předvolby, které tento web uložil pro tento účet</string> + <string name="browser_pill_info_clear">Vymazat data webu</string> + <string name="browser_pill_info_clear_confirm">Odhlásit se z tohoto webu a smazat jeho data?</string> + <string name="backup_action_block_again">Znovu blokovat %1$s</string> + <string name="backup_action_keep_count">Ponechat %1$s</string> + <string name="backup_action_rejoin">Znovu vstoupit (%1$s)</string> + <string name="backup_action_remute">Znovu ztlumit %1$s</string> + <string name="backup_action_restore_count">Obnovit %1$s</string> + <string name="backup_action_restore_feeds">Obnovit kanály</string> + <string name="backup_action_restore_places">Obnovit místa</string> + <string name="backup_action_restore_profile">Obnovit profil</string> + <string name="backup_action_restore_providers">Obnovit poskytovatele</string> + <string name="backup_action_restore_relays">Obnovit relé</string> + <string name="backup_action_restore_topics">Obnovit témata</string> + <string name="backup_action_restore_wallet">Obnovit peněženku</string> + <string name="backup_card_accept">Přijmout</string> + <plurals name="backup_card_and_added"> + <item quantity="one">%1$s a přidali %2$d</item> + <item quantity="few">%1$s a přidali %2$d</item> + <item quantity="many">%1$s a přidali %2$d</item> + <item quantity="other">%1$s a přidali %2$d</item> + </plurals> + <plurals name="backup_card_and_blocked"> + <item quantity="one">%1$s a zablokovali %2$d</item> + <item quantity="few">%1$s a zablokovali %2$d</item> + <item quantity="many">%1$s a zablokovali %2$d</item> + <item quantity="other">%1$s a zablokovali %2$d</item> + </plurals> + <plurals name="backup_card_and_followed"> + <item quantity="one">%1$s a začali sledovat %2$d</item> + <item quantity="few">%1$s a začali sledovat %2$d</item> + <item quantity="many">%1$s a začali sledovat %2$d</item> + <item quantity="other">%1$s a začali sledovat %2$d</item> + </plurals> + <plurals name="backup_card_and_joined"> + <item quantity="one">%1$s a připojili se k %2$d</item> + <item quantity="few">%1$s a připojili se k %2$d</item> + <item quantity="many">%1$s a připojili se k %2$d</item> + <item quantity="other">%1$s a připojili se k %2$d</item> + </plurals> + <plurals name="backup_card_and_muted"> + <item quantity="one">%1$s a ztlumili %2$d</item> + <item quantity="few">%1$s a ztlumili %2$d</item> + <item quantity="many">%1$s a ztlumili %2$d</item> + <item quantity="other">%1$s a ztlumili %2$d</item> + </plurals> + <string name="backup_card_app_settings">Vaše synchronizovaná nastavení Amethystu byla změněna v jiné aplikaci, která není Amethyst. To může způsobit problémy.</string> + <string name="backup_card_concord_emptied">Vyprázdnili jste svůj seznam Concord</string> + <string name="backup_card_concord_rewritten">Změnili jste svůj seznam Concord</string> + <plurals name="backup_card_dm_relays_lost"> + <item quantity="one">Odebrali jste %1$d DM relé</item> + <item quantity="few">Odebrali jste %1$d DM relé</item> + <item quantity="many">Odebrali jste %1$d DM relé</item> + <item quantity="other">Odebrali jste %1$d DM relé</item> + </plurals> + <plurals name="backup_card_feeds_removed"> + <item quantity="one">Odebrali jste %1$d oblíbený kanál</item> + <item quantity="few">Odebrali jste %1$d oblíbené kanály</item> + <item quantity="many">Odebrali jste %1$d oblíbeného kanálu</item> + <item quantity="other">Odebrali jste %1$d oblíbených kanálů</item> + </plurals> + <plurals name="backup_card_follows_dropped"> + <item quantity="one">Přestali jste sledovat %1$d osobu</item> + <item quantity="few">Přestali jste sledovat %1$d osoby</item> + <item quantity="many">Přestali jste sledovat %1$d osoby</item> + <item quantity="other">Přestali jste sledovat %1$d osob</item> + </plurals> + <plurals name="backup_card_hashtags_unfollowed"> + <item quantity="one">Přestali jste sledovat %1$d hashtag</item> + <item quantity="few">Přestali jste sledovat %1$d hashtagy</item> + <item quantity="many">Přestali jste sledovat %1$d hashtagu</item> + <item quantity="other">Přestali jste sledovat %1$d hashtagů</item> + </plurals> + <plurals name="backup_card_indexer_relays_lost"> + <item quantity="one">Odebrali jste %1$d indexační relé</item> + <item quantity="few">Odebrali jste %1$d indexační relé</item> + <item quantity="many">Odebrali jste %1$d indexačního relé</item> + <item quantity="other">Odebrali jste %1$d indexačních relé</item> + </plurals> + <plurals name="backup_card_key_package_relays_lost"> + <item quantity="one">Odebrali jste %1$d relé pro klíče skupinových chatů</item> + <item quantity="few">Odebrali jste %1$d relé pro klíče skupinových chatů</item> + <item quantity="many">Odebrali jste %1$d relé pro klíče skupinových chatů</item> + <item quantity="other">Odebrali jste %1$d relé pro klíče skupinových chatů</item> + </plurals> + <plurals name="backup_card_leave_chats"> + <item quantity="one">Opustili jste %1$d veřejný chat</item> + <item quantity="few">Opustili jste %1$d veřejné chaty</item> + <item quantity="many">Opustili jste %1$d veřejného chatu</item> + <item quantity="other">Opustili jste %1$d veřejných chatů</item> + </plurals> + <plurals name="backup_card_leave_communities"> + <item quantity="one">Opustili jste %1$d komunitu</item> + <item quantity="few">Opustili jste %1$d komunity</item> + <item quantity="many">Opustili jste %1$d komunity</item> + <item quantity="other">Opustili jste %1$d komunit</item> + </plurals> + <plurals name="backup_card_leave_groups"> + <item quantity="one">Opustili jste %1$d skupinu</item> + <item quantity="few">Opustili jste %1$d skupiny</item> + <item quantity="many">Opustili jste %1$d skupiny</item> + <item quantity="other">Opustili jste %1$d skupin</item> + </plurals> + <plurals name="backup_card_leave_rooms"> + <item quantity="one">Opustili jste %1$d chatovací místnost</item> + <item quantity="few">Opustili jste %1$d chatovací místnosti</item> + <item quantity="many">Opustili jste %1$d chatovací místnosti</item> + <item quantity="other">Opustili jste %1$d chatovacích místností</item> + </plurals> + <plurals name="backup_card_mutes_visible"> + <item quantity="one">Zrušili jste ztlumení %1$d položky</item> + <item quantity="few">Zrušili jste ztlumení %1$d položek</item> + <item quantity="many">Zrušili jste ztlumení %1$d položky</item> + <item quantity="other">Zrušili jste ztlumení %1$d položek</item> + </plurals> + <plurals name="backup_card_nip65_lost"> + <item quantity="one">Odebrali jste %1$d veřejné relé</item> + <item quantity="few">Odebrali jste %1$d veřejná relé</item> + <item quantity="many">Odebrali jste %1$d veřejného relé</item> + <item quantity="other">Odebrali jste %1$d veřejných relé</item> + </plurals> + <string name="backup_card_nutzap_key_removed">Odebrali jste svůj klíč pro nutzapy</string> + <string name="backup_card_nutzap_key_replaced">Nahradili jste svůj klíč pro nutzapy</string> + <plurals name="backup_card_nutzap_mints_removed"> + <item quantity="one">Odebrali jste %1$d mint pro nutzapy</item> + <item quantity="few">Odebrali jste %1$d minty pro nutzapy</item> + <item quantity="many">Odebrali jste %1$d mintu pro nutzapy</item> + <item quantity="other">Odebrali jste %1$d mintů pro nutzapy</item> + </plurals> + <plurals name="backup_card_offers_removed"> + <item quantity="one">Odebrali jste %1$d nabídku BOLT12</item> + <item quantity="few">Odebrali jste %1$d nabídky BOLT12</item> + <item quantity="many">Odebrali jste %1$d nabídky BOLT12</item> + <item quantity="other">Odebrali jste %1$d nabídek BOLT12</item> + </plurals> + <plurals name="backup_card_payment_targets_removed"> + <item quantity="one">Odebrali jste %1$d způsob, jak vám zaplatit</item> + <item quantity="few">Odebrali jste %1$d způsoby, jak vám zaplatit</item> + <item quantity="many">Odebrali jste %1$d způsobu, jak vám zaplatit</item> + <item quantity="other">Odebrali jste %1$d způsobů, jak vám zaplatit</item> + </plurals> + <plurals name="backup_card_places_unfollowed"> + <item quantity="one">Přestali jste sledovat %1$d místo</item> + <item quantity="few">Přestali jste sledovat %1$d místa</item> + <item quantity="many">Přestali jste sledovat %1$d místa</item> + <item quantity="other">Přestali jste sledovat %1$d míst</item> + </plurals> + <plurals name="backup_card_private_outbox_lost"> + <item quantity="one">Odebrali jste %1$d soukromé relé</item> + <item quantity="few">Odebrali jste %1$d soukromá relé</item> + <item quantity="many">Odebrali jste %1$d soukromého relé</item> + <item quantity="other">Odebrali jste %1$d soukromých relé</item> + </plurals> + <plurals name="backup_card_profile_lost_fields"> + <item quantity="one">Odebrali jste %1$d pole profilu</item> + <item quantity="few">Odebrali jste %1$d pole profilu</item> + <item quantity="many">Odebrali jste %1$d pole profilu</item> + <item quantity="other">Odebrali jste %1$d polí profilu</item> + </plurals> + <plurals name="backup_card_relay_feeds_lost"> + <item quantity="one">Odebrali jste %1$d kanál relé</item> + <item quantity="few">Odebrali jste %1$d kanály relé</item> + <item quantity="many">Odebrali jste %1$d kanálu relé</item> + <item quantity="other">Odebrali jste %1$d kanálů relé</item> + </plurals> + <plurals name="backup_card_search_relays_lost"> + <item quantity="one">Odebrali jste %1$d vyhledávací relé</item> + <item quantity="few">Odebrali jste %1$d vyhledávací relé</item> + <item quantity="many">Odebrali jste %1$d vyhledávacího relé</item> + <item quantity="other">Odebrali jste %1$d vyhledávacích relé</item> + </plurals> + <plurals name="backup_card_trust_services_changed"> + <item quantity="one">Změnili jste poskytovatele u %1$d služby důvěry</item> + <item quantity="few">Změnili jste poskytovatele u %1$d služeb důvěry</item> + <item quantity="many">Změnili jste poskytovatele u %1$d služby důvěry</item> + <item quantity="other">Změnili jste poskytovatele u %1$d služeb důvěry</item> + </plurals> + <plurals name="backup_card_trusted_lost"> + <item quantity="one">Přestali jste důvěřovat %1$d relé</item> + <item quantity="few">Přestali jste důvěřovat %1$d relé</item> + <item quantity="many">Přestali jste důvěřovat %1$d relé</item> + <item quantity="other">Přestali jste důvěřovat %1$d relé</item> + </plurals> + <plurals name="backup_card_unblocked"> + <item quantity="one">Odblokovali jste %1$d relé</item> + <item quantity="few">Odblokovali jste %1$d relé</item> + <item quantity="many">Odblokovali jste %1$d relé</item> + <item quantity="other">Odblokovali jste %1$d relé</item> + </plurals> + <string name="backup_card_wallet_emptied">Vyprázdnili jste svou peněženku Cashu</string> + <string name="backup_card_wallet_rewritten">Změnili jste svou peněženku Cashu</string> + <string name="backup_conflict_added">Přidáno (%1$s)</string> + <string name="backup_conflict_changed">Změněno (%1$s)</string> + <string name="backup_conflict_count_added">%1$s přidáno</string> + <string name="backup_conflict_count_changed">%1$s změněno</string> + <string name="backup_conflict_count_removed">%1$s odebráno</string> + <string name="backup_conflict_keep_new">Použít novou verzi</string> + <string name="backup_conflict_private_added">Přidali jste soukromé (šifrované) položky.</string> + <string name="backup_conflict_private_changed">Změnili jste také soukromé (šifrované) položky. Zde je nelze porovnat.</string> + <string name="backup_conflict_private_cleared">Odebrali jste všechny soukromé (šifrované) položky.</string> + <string name="backup_conflict_relay_read_only">jen čtení</string> + <string name="backup_conflict_relay_read_write">čtení i zápis</string> + <string name="backup_conflict_relay_write_only">jen zápis</string> + <string name="backup_conflict_removed">Odebráno (%1$s)</string> + <string name="backup_conflict_restore_mine">Obnovit uloženou verzi</string> + <string name="backup_conflict_title">V jiné aplikaci jste změnili: %1$s</string> + <string name="backup_entry_algo_feed">Kanály</string> + <string name="backup_entry_chat_room">Chatovací místnosti</string> + <string name="backup_entry_community">Komunity</string> + <string name="backup_entry_hashtag">Hashtagy</string> + <string name="backup_entry_identity_claim">Propojené identity</string> + <string name="backup_entry_location">Místa</string> + <string name="backup_entry_mint">Minty</string> + <string name="backup_entry_nutzap_key">Klíče pro nutzapy</string> + <string name="backup_entry_offer">Nabídky</string> + <string name="backup_entry_other">Ostatní položky</string> + <string name="backup_entry_payment_target">Platební adresy</string> + <string name="backup_entry_person">Lidé</string> + <string name="backup_entry_profile_field">Pole profilu</string> + <string name="backup_entry_public_chat">Veřejné chaty</string> + <string name="backup_entry_relay">Relé</string> + <string name="backup_entry_relay_group">Skupiny</string> + <string name="backup_entry_thread">Vlákna</string> + <string name="backup_entry_trust_provider">Poskytovatelé důvěry</string> + <string name="backup_entry_word">Slova</string> + <string name="backup_profile_field_about">O mně</string> + <string name="backup_profile_field_banner">Banner</string> + <string name="backup_profile_field_birthday">Narozeniny</string> + <string name="backup_profile_field_bot">Příznak bota</string> + <string name="backup_profile_field_clink_offer">Nabídka CLINK</string> + <string name="backup_profile_field_display_name">Zobrazované jméno</string> + <string name="backup_profile_field_lud06">LNURL</string> + <string name="backup_profile_field_lud16">Lightning adresa</string> + <string name="backup_profile_field_name">Uživatelské jméno</string> + <string name="backup_profile_field_nip05">Nostr adresa</string> + <string name="backup_profile_field_picture">Obrázek</string> + <string name="backup_profile_field_pronouns">Zájmena</string> + <string name="backup_profile_field_website">Webová stránka</string> + <string name="backup_review_allowed_tag">Povoleno</string> + <string name="backup_review_changed_by_other_app">Změněno v jiné aplikaci · %1$s</string> + <string name="backup_review_concord_body">Vaše komunity Concord jsou šifrované pro vás, takže je zde nelze porovnat. Obnovení vrátí přesně to, co uložilo toto zařízení.</string> + <string name="backup_review_demoted_tag">Přesunuto</string> + <string name="backup_review_dropped_count">Vypuštěno: %1$s</string> + <string name="backup_review_dropped_tag">Vypuštěno</string> + <string name="backup_review_encrypted_added">Vytvořeno vámi</string> + <string name="backup_review_encrypted_changed">Změněno vámi</string> + <string name="backup_review_encrypted_cleared">Vyprázdněno vámi</string> + <string name="backup_review_feed">Kanál</string> + <string name="backup_review_gone">Odstraněno</string> + <string name="backup_review_hashtags_you_follow">Hashtagy, které sledujete</string> + <string name="backup_review_inbox">Inbox</string> + <string name="backup_review_inbox_explainer">kde vás ostatní zastihnou</string> + <string name="backup_review_joined">připojeno</string> + <string name="backup_review_kept">zachováno</string> + <string name="backup_review_kept_count">Zachováno: %1$s</string> + <string name="backup_review_key_removed">Přijímací klíč odebrán</string> + <string name="backup_review_key_replaced">Přijímací klíč nahrazen</string> + <string name="backup_review_key_warning">Nutzapy budou uzamčeny tímto novým klíčem. Pokud ho toto zařízení nemá, možná je zde nebudete moci uplatnit.</string> + <string name="backup_review_left">opuštěno</string> + <string name="backup_review_left_tag">Opuštěno</string> + <string name="backup_review_listed_tag">Uvedeno</string> + <string name="backup_review_moved_tag">Přesunuto</string> + <string name="backup_review_names_and_more">%1$s a další (%2$s)</string> + <string name="backup_review_new">Nová verze</string> + <string name="backup_review_new_count">Nové: %1$s</string> + <string name="backup_review_new_count_caption">nové</string> + <string name="backup_review_new_tag">Nové</string> + <string name="backup_review_newly_blocked">Nově blokováno</string> + <string name="backup_review_newly_muted">Nově ztlumeno: %1$s</string> + <string name="backup_review_no_longer_blocked">Již neblokováno</string> + <string name="backup_review_not_set">Nenastaveno</string> + <string name="backup_review_nothing_here">Nic tu není</string> + <string name="backup_review_offer">Nabídka BOLT12</string> + <plurals name="backup_review_offers_gone"> + <item quantity="one">%1$d nabídka odebrána</item> + <item quantity="few">%1$d nabídky odebrány</item> + <item quantity="many">%1$d nabídky odebráno</item> + <item quantity="other">%1$d nabídek odebráno</item> + </plurals> + <string name="backup_review_outbox">Outbox</string> + <string name="backup_review_outbox_explainer">kam publikujete</string> + <plurals name="backup_review_payment_targets_gone"> + <item quantity="one">Odebrán %1$d způsob, jak vám zaplatit</item> + <item quantity="few">Odebrány %1$d způsoby, jak vám zaplatit</item> + <item quantity="many">Odebráno %1$d způsobu, jak vám zaplatit</item> + <item quantity="other">Odebráno %1$d způsobů, jak vám zaplatit</item> + </plurals> + <string name="backup_review_payments_explainer">Lidé, kteří vám platí, vidí jen to, co je v tomto seznamu.</string> + <string name="backup_review_places_gone">odebraná místa</string> + <string name="backup_review_places_you_follow">Místa, která sledujete</string> + <string name="backup_review_private_added_body">Nyní jsou šifrované — nejspíš právě sem se přesunuly veřejné položky výše.</string> + <string name="backup_review_private_added_title">Převedli jste položky na soukromé</string> + <string name="backup_review_private_changed_body">Jsou šifrované, takže je zde nelze porovnat.</string> + <string name="backup_review_private_changed_title">Změnili jste soukromé položky</string> + <string name="backup_review_private_wiped_body">Jsou šifrované, takže jen vy můžete vidět, co obsahovaly. Obnovení je všechny vrátí.</string> + <string name="backup_review_private_wiped_title">Odebrali jste všechny soukromé položky</string> + <string name="backup_review_receiving_tag">Přijímá</string> + <string name="backup_review_renamed">přejmenováno</string> + <string name="backup_review_renamed_tag">Přejmenováno</string> + <string name="backup_review_saved">V tomto zařízení</string> + <plurals name="backup_review_search_people"> + <item quantity="one">Prohledat %1$s osobu</item> + <item quantity="few">Prohledat %1$s osoby</item> + <item quantity="many">Prohledat %1$s osoby</item> + <item quantity="other">Prohledat %1$s osob</item> + </plurals> + <string name="backup_review_searching_tag">Vyhledávání</string> + <string name="backup_review_service">Služba</string> + <string name="backup_review_service_followers">Sledující</string> + <string name="backup_review_service_rank">Hodnocení</string> + <string name="backup_review_service_topics">Témata</string> + <plurals name="backup_review_services_changed"> + <item quantity="one">Změněno %1$s z %2$s služby</item> + <item quantity="few">Změněno %1$s z %2$s služeb</item> + <item quantity="many">Změněno %1$s z %2$s služby</item> + <item quantity="other">Změněno %1$s z %2$s služeb</item> + </plurals> + <string name="backup_review_services_changed_explainer">Nyní je poskytuje někdo jiný, nebo nikdo</string> + <string name="backup_review_still_blocked">Stále blokováno</string> + <string name="backup_review_storing_tag">Ukládá</string> + <string name="backup_review_tab_dropped">Vypuštěno · %1$s</string> + <string name="backup_review_tab_edited">Upraveno · %1$s</string> + <string name="backup_review_tab_new">Nové · %1$s</string> + <string name="backup_review_title_cleared">%1$s – vymazáno</string> + <string name="backup_review_title_updated">%1$s – aktualizováno</string> + <string name="backup_review_topics_gone">odebraná témata</string> + <string name="backup_review_trust_warning">Hodnocení, počty sledujících a filtrování spamu, které vidíte, pocházejí od těchto poskytovatelů.</string> + <string name="backup_review_trusted_tag">Důvěryhodné</string> + <string name="backup_review_unblocked">%1$s odblokováno</string> + <string name="backup_review_unblocked_explainer">Amethyst se k nim znovu připojí</string> + <string name="backup_review_units_tag">Jednotky</string> + <string name="backup_review_unmuted">%1$s odztlumeno</string> + <string name="backup_review_unmuted_explainer">Tyto položky se opět mohou objevit ve vašich kanálech</string> + <string name="backup_review_untrusted_tag">Nedůvěryhodné</string> + <string name="backup_review_wallet_body">Minty a klíče vaší peněženky jsou šifrované pro vás, takže je zde nelze porovnat položku po položce. Obnovení vrátí přesně to, co uložilo toto zařízení.</string> + <string name="backup_review_was_named">dříve „%1$s“</string> + <string name="backup_review_words_and_hashtags">Slova a hashtagy</string> + <string name="backup_type_app_settings">synchronizovaná nastavení aplikace</string> + <string name="backup_type_app_settings_explainer">Nastavení Amethystu synchronizovaná mezi vašimi zařízeními. Jsou šifrovaná, takže lze porovnat jen to, zda existují.</string> + <string name="backup_type_blocked_relays">seznam blokovaných relé</string> + <string name="backup_type_blocked_relays_explainer">Relé, ke kterým se tato aplikace nikdy nepřipojí.</string> + <string name="backup_type_bolt12_offers">nabídky BOLT12</string> + <string name="backup_type_bolt12_offers_explainer">Lightning nabídky, pomocí kterých vám ostatní platí.</string> + <string name="backup_type_cashu_wallet">peněženka Cashu</string> + <string name="backup_type_cashu_wallet_explainer">Nastavení a minty vaší ecash peněženky. Jsou šifrované, takže lze porovnat jen to, zda existují.</string> + <string name="backup_type_communities">seznam komunit</string> + <string name="backup_type_communities_explainer">Komunity, ke kterým jste se připojili.</string> + <string name="backup_type_concord_communities">seznam komunit Concord</string> + <string name="backup_type_concord_communities_explainer">Komunity Concord, ke kterým jste se připojili. Jsou šifrované, takže lze porovnat jen to, zda existují.</string> + <string name="backup_type_dm_relays">seznam DM relé</string> + <string name="backup_type_dm_relays_explainer">Kam vám ostatní doručují soukromé zprávy.</string> + <string name="backup_type_ephemeral_chats">seznam dočasných chatů</string> + <string name="backup_type_ephemeral_chats_explainer">Dočasné chatovací místnosti, ke kterým jste se připojili.</string> + <string name="backup_type_favorite_algo_feeds">seznam oblíbených kanálů</string> + <string name="backup_type_favorite_algo_feeds_explainer">Algoritmické kanály, které jste si uložili.</string> + <string name="backup_type_follow_list">seznam sledovaných</string> + <string name="backup_type_follow_list_explainer">Určuje, čí příspěvky se zobrazí ve vašem domovském kanálu.</string> + <string name="backup_type_geohashes">sledovaná místa</string> + <string name="backup_type_geohashes_explainer">Místa, jejichž příspěvky se zobrazují ve vašich kanálech.</string> + <string name="backup_type_hashtags">sledované hashtagy</string> + <string name="backup_type_hashtags_explainer">Hashtagy, jejichž příspěvky se zobrazují ve vašich kanálech.</string> + <string name="backup_type_indexer_relays">seznam indexačních relé</string> + <string name="backup_type_indexer_relays_explainer">Relé používaná k hledání profilů a seznamů relé ostatních lidí.</string> + <string name="backup_type_key_package_relays">relé pro klíče skupinových chatů</string> + <string name="backup_type_key_package_relays_explainer">Kde ostatní najdou klíče, aby vás mohli přidat do šifrovaných skupinových chatů.</string> + <string name="backup_type_mute_list">seznam ztlumených</string> + <string name="backup_type_mute_list_explainer">Skrývá lidi, slova, hashtagy a vlákna ze všeho, co vidíte.</string> + <string name="backup_type_nutzap_info">nastavení příjmu nutzapů</string> + <string name="backup_type_nutzap_info_explainer">Minty a klíč, které ostatní používají k posílání nutzapů vám.</string> + <string name="backup_type_other">seznam</string> + <string name="backup_type_other_explainer">Seznam uložený v tomto zařízení.</string> + <string name="backup_type_outbox_inbox_relays">seznam veřejných relé</string> + <string name="backup_type_outbox_inbox_relays_explainer">Říká všem, kde najdou vaše příspěvky a kam vám posílat odpovědi a zmínky.</string> + <string name="backup_type_payment_targets">platební adresy</string> + <string name="backup_type_payment_targets_explainer">Způsoby, jak vám ostatní mohou zaplatit.</string> + <string name="backup_type_private_outbox_relays">seznam soukromých relé</string> + <string name="backup_type_private_outbox_relays_explainer">Kde jsou uloženy vaše koncepty a soukromá data.</string> + <string name="backup_type_profile">profil</string> + <string name="backup_type_profile_explainer">Vaše jméno, obrázek, bio a platební adresy tak, jak je vidí všichni.</string> + <string name="backup_type_public_chats">seznam veřejných chatů</string> + <string name="backup_type_public_chats_explainer">Veřejné chaty, ke kterým jste se připojili.</string> + <string name="backup_type_relay_feeds">oblíbené kanály relé</string> + <string name="backup_type_relay_feeds_explainer">Relé, která procházíte jako kanály.</string> + <string name="backup_type_relay_groups">seznam skupin</string> + <string name="backup_type_relay_groups_explainer">Skupiny na relé, ke kterým jste se připojili.</string> + <string name="backup_type_search_relays">seznam vyhledávacích relé</string> + <string name="backup_type_search_relays_explainer">Relé používaná při vyhledávání.</string> + <string name="backup_type_trust_providers">seznam poskytovatelů důvěry</string> + <string name="backup_type_trust_providers_explainer">Služby, kterým důvěřujete, že pro vás hodnotí a ověřují lidi.</string> + <string name="backup_type_trusted_relays">seznam důvěryhodných relé</string> + <string name="backup_type_trusted_relays_explainer">Relé, ke kterým se připojujete bez dotazu.</string> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-de-rDE/strings.xml b/commonsUI/src/commonMain/composeResources/values-de-rDE/strings.xml index 94482ed9f7..64ad7f8fd8 100644 --- a/commonsUI/src/commonMain/composeResources/values-de-rDE/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-de-rDE/strings.xml @@ -4924,4 +4924,1174 @@ one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> + <string name="geocache_unnamed">Geocache</string> + <string name="geocache_type_traditional">Traditional</string> + <string name="geocache_type_multi">Multi-Cache</string> + <string name="geocache_type_mystery">Mystery</string> + <string name="geocache_size_micro">Mikro</string> + <string name="geocache_size_small">Klein</string> + <string name="geocache_size_regular">Normal</string> + <string name="geocache_size_large">Groß</string> + <string name="geocache_size_other">Andere Größe</string> + <string name="geocache_difficulty_short">D%1$d</string> + <string name="geocache_terrain_short">T%1$d</string> + <string name="geocache_first_to_find">FTF</string> + <string name="geocache_is_art">Kunstwerk</string> + <string name="geocache_mission">Key Quest</string> + <string name="geocache_needs_verification">Verifizierte Funde</string> + <string name="geocache_claimed">FTF vergeben</string> + <string name="geocache_archived">Archiviert</string> + <string name="geocache_hint_tap_to_reveal">Nicht weiter? Tippen, um den Hinweis anzuzeigen</string> + <string name="geocache_hint_other_reading">Nicht lesbar? Für die andere Lesart tippen</string> + <string name="geocache_nearby">In der Nähe</string> + <string name="geocache_found_it">Gefunden!</string> + <string name="geocache_verified_find">Verifizierter Fund</string> + <string name="geocache_invalid_proof">Nachweis ist ungültig</string> + <string name="geocache_loading">Cache wird geladen…</string> + <string name="geocache_hidden_by">Versteckt von %1$s</string> + <string name="geocache_navigate">Navigieren</string> + <string name="geocache_copy_coordinates">Koordinaten kopieren</string> + <string name="geocache_coordinates_copied">Koordinaten kopiert</string> + <string name="geocache_photos_section">Fotos</string> + <string name="geocache_mission_section">Key-Quest-Mission</string> + <string name="geocache_logs_section">Logs</string> + <string name="geocache_no_logs">Diesen Cache hat noch niemand geloggt. Sei der Erste.</string> + <string name="geocache_i_found_it">Ich habe ihn gefunden</string> + <string name="geocache_didnt_find_it">Nicht gefunden</string> + <string name="geocache_add_note">Notiz hinzufügen</string> + <string name="geocache_needs_maintenance">Wartungsbedarf melden</string> + <string name="geocache_add_to_hunt">Zu einer Tour hinzufügen</string> + <string name="geocache_hunt_new">Neue Tour…</string> + <string name="geocache_hunt_none_yet">Du hast noch keine Tour erstellt.</string> + <string name="geocache_hunt_already_on">Bereits in dieser Tour</string> + <plurals name="geocache_caches_here"> + <item quantity="one">%1$d Cache hier</item> + <item quantity="other">%1$d Caches hier</item> + </plurals> + <string name="geocache_photo_remove">Dieses Foto entfernen</string> + <string name="geocache_archived_notice">Dieser Cache wurde archiviert. Er ist nicht mehr im Spiel.</string> + <string name="geocache_ftf_won_by">FTF geht an %1$s</string> + <string name="geocache_ftf_available">Der FTF ist noch zu haben</string> + <string name="geocache_owner_actions">Owner</string> + <string name="geocache_owner_edit">Diesen Cache bearbeiten</string> + <string name="geocache_owner_archive">Diesen Cache archivieren</string> + <string name="geocache_owner_archive_confirm">Archivieren. Finder sehen, dass er nicht mehr im Spiel ist, und das Listing bleibt lesbar.</string> + <string name="geocache_owner_lock_ftf">Erstfinder festschreiben</string> + <string name="geocache_owner_lock_ftf_explain">Veröffentlicht einen F-Tag mit %1$s. Das ist endgültig.</string> + <string name="geocache_owner_no_finders">Noch keine verifizierten Funde, also gibt es niemanden zum Festschreiben.</string> + <string name="geocache_owner_show_qr">Verifizierungscode anzeigen</string> + <string name="geocache_owner_qr_warning">Drucke ihn aus und lege ihn in den Cache. Amethyst speichert ihn nicht — wenn du ihn verlierst, können Finder nicht mehr beweisen, dass sie dort waren.</string> + <string name="geocache_log_type_dnf">Nicht gefunden</string> + <string name="geocache_log_type_note">Notiz</string> + <string name="geocache_log_type_maintenance">Wartung erforderlich</string> + <string name="geocache_log_title">Fund loggen</string> + <string name="geocache_log_cache_label">Cache</string> + <string name="geocache_log_your_log">Dein Log</string> + <string name="geocache_log_placeholder">Wie lief es?</string> + <string name="geocache_log_add_photo">Foto hinzufügen</string> + <string name="geocache_log_mission_answer">Deine Key-Quest-Antwort</string> + <string name="geocache_log_proof_section">Anwesenheitsnachweis</string> + <string name="geocache_log_scan_code">Code im Cache scannen</string> + <string name="geocache_log_scan_again">Anderen Code scannen</string> + <string name="geocache_log_proof_verified">Verifiziert — du warst am Cache</string> + <string name="geocache_log_proof_explain">Einmalig mit dem Code aus der Dose signiert. Der Schlüssel wird nie gespeichert und verlässt diesen Bildschirm nie.</string> + <string name="geocache_log_proof_bad_code">Das ist kein Verifizierungscode.</string> + <string name="geocache_log_proof_wrong_cache">Dieser Code gehört zu einem anderen Cache.</string> + <string name="geocache_log_proof_optional">Dieser Cache verlangt keinen Nachweis, du kannst ihn also einfach loggen.</string> + <string name="geocache_log_no_location_attached">Dein eigener Standort wird diesem Log nicht angehängt.</string> + <string name="geocache_log_post">Senden</string> + <string name="geocache_log_comment_title">Log hinzufügen</string> + <string name="geocache_tab_nearby">In der Nähe</string> + <string name="geocache_tab_map">Karte</string> + <string name="geocache_tab_hunts">Touren</string> + <string name="geocache_tab_finds">Funde</string> + <string name="geocache_tab_mine">Meine</string> + <string name="geocache_filter_unfound">Nicht gefunden</string> + <string name="geocache_filter_difficulty">D ≤ %1$d</string> + <string name="geocache_filter_terrain">T ≤ %1$d</string> + <string name="geocache_filter_has_prize">Mit Preis</string> + <string name="geocache_filter_verified_only">Nachweis erforderlich</string> + <string name="geocache_use_my_location">Meinen Standort verwenden</string> + <string name="geocache_sorted_by_distance">Nach Entfernung sortiert</string> + <string name="geocache_sorted_by_recent">Neueste zuerst</string> + <string name="geocache_hide_a_cache">Cache verstecken</string> + <string name="geocache_empty_nearby">Hier gibt es noch keine Caches. Probiere eine größere Folgeliste oder verstecke den ersten.</string> + <string name="geocache_empty_finds">Du hast noch keinen Fund geloggt.</string> + <string name="geocache_empty_mine">Du hast noch keinen Cache versteckt.</string> + <string name="geocache_empty_hunts">Noch keine Touren.</string> + <string name="geocache_stat_finds">Funde</string> + <string name="geocache_stat_ftf">FTFs</string> + <string name="geocache_stat_verified">Verifiziert</string> + <string name="geocache_stat_hidden">Versteckt</string> + <string name="geocache_map_recenter">Auf mich zentrieren</string> + <string name="geocache_map_long_press">Lange auf die Karte drücken, um dort einen Cache zu verstecken</string> + <string name="geocache_map_open">Öffnen</string> + <string name="geocache_new_section_where">Wo</string> + <string name="geocache_new_section_what">Was</string> + <string name="geocache_new_section_extras">Extras</string> + <string name="geocache_new_section_proof">Nachweis</string> + <string name="geocache_new_section_relays">Relays</string> + <string name="geocache_new_location_warning">Dies veröffentlicht einen öffentlichen Standort mit einer Genauigkeit von etwa 5 Metern. Jeder auf Nostr kann ihn sehen, und ein veröffentlichtes Event kann nicht zurückgeholt werden.</string> + <string name="geocache_new_pick_location">Ort auswählen</string> + <string name="geocache_new_ladder">Veröffentlicht %1$s sowie gröbere Zellen bis hinunter zu 3 Zeichen für die Umkreissuche.</string> + <string name="geocache_new_name">Name</string> + <string name="geocache_new_description">Beschreibung</string> + <string name="geocache_new_type">Typ</string> + <string name="geocache_new_size">Größe</string> + <string name="geocache_new_difficulty">Schwierigkeit</string> + <string name="geocache_new_terrain">Gelände</string> + <string name="geocache_new_hint">Hinweis</string> + <string name="geocache_new_hint_preview">Finder sehen</string> + <string name="geocache_new_hint_scramble">Hinweis verschlüsseln (ROT13)</string> + <string name="geocache_new_mission">Key-Quest-Mission</string> + <string name="geocache_new_modifiers">Zusätze</string> + <string name="geocache_new_require_proof">Anwesenheitsnachweis verlangen</string> + <string name="geocache_new_proof_ready">Für diesen Cache wurde ein Verifizierungsschlüssel erzeugt.</string> + <string name="geocache_new_log_relays">Wo Finder loggen sollen</string> + <string name="geocache_new_publish">Veröffentlichen</string> + <string name="geocache_new_needs_name">Gib dem Cache einen Namen.</string> + <string name="geocache_new_needs_location">Wähle aus, wo der Cache versteckt ist.</string> + <string name="geocache_hunt_progress">%1$d von %2$d gefunden</string> + <string name="geocache_hunt_start">Tour starten</string> + <plurals name="geocache_hunt_caches"> + <item quantity="one">%1$d Cache</item> + <item quantity="other">%1$d Caches</item> + </plurals> + <string name="geocache_hunt_title">Titel</string> + <string name="geocache_hunt_description">Beschreibung</string> + <string name="geocache_hunt_banner">Bannerbild</string> + <string name="geocache_hunt_pick_caches">Caches, in Reihenfolge</string> + <string name="geocache_hunt_add_cache">Cache hinzufügen</string> + <string name="geocache_hunt_move_up">Nach oben</string> + <string name="geocache_hunt_move_down">Nach unten</string> + <string name="geocache_hunt_remove">Entfernen</string> + <string name="geocache_hunt_theme">Seitendesign</string> + <string name="geocache_hunt_map_style">Kartenstil</string> + <string name="geocache_hunt_needs_title">Gib der Tour einen Titel.</string> + <string name="geocache_hunt_needs_caches">Füge mindestens einen Cache hinzu.</string> + <string name="geocache_publish_failed">Veröffentlichen fehlgeschlagen. Prüfe deine Relay-Verbindung und versuche es erneut.</string> + <string name="cordn_groups_title">Cordn-Gruppen</string> + <string name="cordn_badge_coordinator">Koordinator-Gruppe</string> + <string name="cordn_badge_coordinator_desc">Zugestellt von einem Koordinator, nicht von Relays</string> + <string name="cordn_exposure_title">Was dieser Koordinator sehen kann</string> + <string name="cordn_exposure_subtitle">Ein Koordinator ist ein einzelner Server, der jede Nachricht dieser Gruppe in Reihenfolge überträgt. Das ist ein anderer Kompromiss als bei Relays — gut zu wissen, bevor du dies wie jeden anderen Chat behandelst.</string> + <string name="cordn_exposure_dimension_content">Nachrichteninhalte</string> + <string name="cordn_exposure_dimension_membership">Wer in der Gruppe ist</string> + <string name="cordn_exposure_dimension_messaging">Wer was sendet</string> + <string name="cordn_exposure_level_none">Nicht lesbar</string> + <string name="cordn_exposure_level_pseudonymous">Unter einem Wegwerfschlüssel</string> + <string name="cordn_exposure_level_identified">Mit deinem echten Konto verknüpft</string> + <string name="cordn_exposure_details">Details</string> + <string name="cordn_exposure_coordinator">Koordinator %1$s</string> + <string name="cordn_exposure_differs">Das ist nicht dieselbe Offenlegung wie bei einer Marmot-Gruppe, bei der kein einzelner Betreiber die gesamte Unterhaltung besitzt.</string> + <string name="cordn_note_membership">Beim Beitritt werden auf beiden Seiten echte Konten genannt, daher weiß der Koordinator, wer in dieser Gruppe ist.</string> + <string name="cordn_note_linked">Ein einziger Wegwerfschlüssel sendet und ruft für jede deiner Gruppen hier ab, daher kann der Koordinator erkennen, dass sie alle zu dir gehören.</string> + <string name="cordn_note_history">Ein einzelner Betreiber besitzt den vollständigen, geordneten Verlauf jeder Gruppe, die er überträgt.</string> + <string name="cordn_note_publication">Das von dir veröffentlichte KeyPackage ist ein signierter Nachweis, dass dieses Konto cordn nutzt, und der Koordinator kann es an jeden weitergeben.</string> + <string name="cordn_note_sizes">Nachrichten werden nicht aufgefüllt, daher sieht der Koordinator, wie lang jede einzelne ist.</string> + <string name="cordn_note_encryption_bug">Dieser Client verschlüsselt nicht für den Koordinator. Das ist ein Fehler — bitte melde ihn.</string> + <string name="cordn_health_unknown">Noch nicht kontaktiert</string> + <string name="cordn_health_ok">Antwortet</string> + <string name="cordn_health_down">Antwortet nicht</string> + <plurals name="cordn_health_failures"> + <item quantity="one">%1$d fehlgeschlagener Versuch in Folge</item> + <item quantity="other">%1$d fehlgeschlagene Versuche in Folge</item> + </plurals> + <string name="cordn_link_explainer">Füge einen cordn1…-Link ein, den dir jemand geschickt hat, um zu sehen, welcher Koordinator diese Gruppe überträgt und was dieser Betreiber über dich erfahren würde, wenn du beitrittst.</string> + <string name="cordn_link_field">cordn1…</string> + <string name="cordn_link_inspect">Prüfen</string> + <string name="cordn_link_paste">Einfügen</string> + <string name="cordn_link_clear">Leeren</string> + <string name="cordn_link_invalid">Das ist kein cordn-Gruppenlink: %1$s</string> + <string name="cordn_link_group_id">Gruppen-ID</string> + <string name="cordn_link_coordinator">Koordinator</string> + <string name="cordn_link_relays">Erreichbar über</string> + <string name="cordn_link_no_coordinator">Dieser Link nennt keinen Koordinator und kann daher nicht allein verwendet werden. Frag die Person, die ihn geschickt hat, welcher Koordinator die Gruppe überträgt.</string> + <string name="cordn_link_not_joinable">Durch das Prüfen eines Links trittst du nichts bei, und ein Link allein macht dich nicht zum Mitglied.</string> + <string name="cordn_link_title">Cordn-Gruppenlink</string> + <string name="cordn_link_search_keywords">cordn koordinator coordinator gruppe link einladung invite mls chat metadaten privatsphäre offenlegung</string> + <string name="cordn_coordinators_title">Cordn-Koordinatoren</string> + <string name="cordn_coordinators_search_keywords">cordn koordinator coordinator hinzufügen entfernen löschen purge status server mls chat reihenfolge</string> + <string name="cordn_keypackages_title">Cordn-KeyPackages</string> + <string name="cordn_keypackages_search_keywords">cordn keypackage schlüsselpaket veröffentlichen einladen einladung last resort koordinator mls</string> + <string name="cordn_backup_title">Cordn-Backup</string> + <string name="cordn_backup_search_keywords">cordn backup sicherung wiederherstellen export import passphrase gruppen wiederherstellung</string> + <string name="cordn_group_untitled">Gruppe %1$s</string> + <string name="cordn_group_no_messages_yet">Noch keine Nachrichten</string> + <string name="cordn_group_via_coordinator">Cordn-Gruppe, übertragen von %1$s</string> + <string name="cordn_preview_deleted">Nachricht gelöscht</string> + <string name="cordn_preview_voice_note">Sprachnachricht</string> + <string name="cordn_preview_photo">Foto</string> + <string name="cordn_preview_video">Video</string> + <string name="cordn_preview_file">Datei</string> + <string name="cordn_invitations_entry">In eine cordn-Gruppe eingeladen?</string> + <string name="cordn_invitations_entry_action">Einladungen ansehen</string> + <string name="cordn_hub_title">Cordn</string> + <string name="cordn_hub_explainer">Gruppen, die von einem Koordinator deiner Wahl statt von Relays zugestellt werden.</string> + <string name="cordn_hub_link">Gruppenlink prüfen</string> + <string name="cordn_hub_link_desc">Sieh nach, worauf ein cordn1…-Link verweist, bevor du darauf reagierst</string> + <string name="cordn_migrate_section_send">Von diesem Smartphone umziehen</string> + <string name="cordn_migrate_section_receive">Von einem anderen Smartphone übernehmen</string> + <string name="cordn_keypackages_section">Veröffentlichte KeyPackages</string> + <string name="cordn_link_section_input">Der Link</string> + <string name="cordn_coordinators_section_yours">Deine Koordinatoren</string> + <string name="cordn_coordinators_section_discover">Selbst angekündigt</string> + <string name="cordn_coordinators_section_manual">Manuell hinzufügen</string> + <string name="cordn_backup_section_passphrase">Passphrase</string> + <string name="cordn_backup_section_export">Backup erstellen</string> + <string name="cordn_backup_section_restore">Aus Backup wiederherstellen</string> + <string name="cordn_hub_section_service">Der Koordinator</string> + <string name="cordn_hub_section_device">Dieses Gerät</string> + <string name="cordn_hub_coordinators">Koordinatoren</string> + <string name="cordn_hub_coordinators_desc">Die Server, die deine Gruppen übertragen</string> + <string name="cordn_hub_keypackages">KeyPackages</string> + <string name="cordn_hub_keypackages_desc">Damit können andere dich zu einer Gruppe hinzufügen</string> + <string name="cordn_hub_backup">Backup und Wiederherstellung</string> + <string name="cordn_hub_backup_desc">Eine mit Passphrase verschlüsselte Datei, die du aufbewahrst</string> + <string name="cordn_hub_migrate">Auf ein neues Smartphone umziehen</string> + <string name="cordn_hub_migrate_desc">Übergib deine Gruppen an ein Gerät, auf das du wechselst</string> + <string name="cordn_migrate_title">Auf ein neues Smartphone umziehen</string> + <string name="cordn_migrate_explainer">Deine cordn-Gruppen liegen auf diesem Gerät. Für den Umzug braucht es einen Code: Dieses Smartphone veröffentlicht sie, das neue Smartphone liest sie ein, und dieses Smartphone hört auf.</string> + <string name="cordn_migrate_sign_in_first">Melde dich zuerst auf dem neuen Smartphone mit demselben Konto an. Umgezogen werden deine Gruppen, niemals dein Schlüssel.</string> + <string name="cordn_migrate_export">Übergabe starten</string> + <string name="cordn_migrate_exporting">Wird veröffentlicht…</string> + <string name="cordn_migrate_scan_this">Scanne dies auf dem neuen Smartphone</string> + <string name="cordn_migrate_code_note">Der Code ist nur ein Verweis. Wer ihn fotografiert, erfährt nichts — das, worauf er verweist, ist für dich verschlüsselt.</string> + <string name="cordn_migrate_handed_off">Dieses Gerät hat seine Gruppen übergeben</string> + <string name="cordn_migrate_handed_off_desc">Es sendet und empfängt nicht mehr, denn wenn zwei Smartphones aus derselben Gruppe senden, geht sie für alle darin kaputt. Deine Gruppen sind noch auf diesem Gerät, und nichts wurde gelöscht.</string> + <string name="cordn_migrate_cancel">Dieses Gerät zurückholen</string> + <string name="cordn_migrate_cancel_desc">Verwende dies, wenn der Umzug nicht abgeschlossen wurde. Nur, wenn das neue Smartphone noch nicht mit dem Senden begonnen hat.</string> + <string name="cordn_migrate_receive">Von einem anderen Smartphone empfangen</string> + <string name="cordn_migrate_scan">Code scannen</string> + <string name="cordn_migrate_paste">oder einfügen</string> + <string name="cordn_migrate_import">Gruppen hierher holen</string> + <string name="cordn_migrate_importing">Wird abgerufen…</string> + <string name="cordn_migrate_replaces_warning">Dies ersetzt alle cordn-Gruppen, die bereits auf diesem Smartphone sind. Sie können nicht zusammengeführt werden.</string> + <plurals name="cordn_migrate_done"> + <item quantity="one">%1$d Gruppe umgezogen</item> + <item quantity="other">%1$d Gruppen umgezogen</item> + </plurals> + <string name="cordn_migrate_no_groups">Auf diesem Gerät gibt es keine cordn-Gruppen zum Umziehen.</string> + <string name="cordn_migrate_no_server">Lege zuerst einen Medienserver fest — die verschlüsselten Dokumente brauchen einen Ablageort, während das andere Smartphone sie abruft.</string> + <string name="cordn_migrate_exposure_title">Was dieses Gerät verlässt</string> + <string name="cordn_migrate_exposure_body">Dein Gruppenzustand wird verschlüsselt und auf deinen Medienserver hochgeladen, damit das andere Smartphone ihn abrufen kann. Für diesen Server ist er nicht lesbar, er sieht nur eine Größe und einen Zeitpunkt. Lösche die Blobs danach, wenn nichts zurückbleiben soll.</string> + <string name="cordn_action_delete">Löschen</string> + <string name="cordn_action_edit">Bearbeiten</string> + <string name="cordn_action_editing">Deine Nachricht wird bearbeitet</string> + <string name="cordn_action_go_to_message">Zur Nachricht</string> + <string name="cordn_action_pin">Anheften</string> + <string name="cordn_action_reply">Antworten</string> + <string name="cordn_action_unpin">Loslösen</string> + <string name="cordn_admin_action_failed">Diese Änderung konnte nicht vorgenommen werden.</string> + <string name="cordn_admin_coordinator_silent">Der Koordinator hat nicht geantwortet, daher wurde nichts geändert. Versuche es gleich noch einmal.</string> + <string name="cordn_admin_no_key_package">%1$s hat kein KeyPackage bei diesem Koordinator veröffentlicht und kann daher noch nicht hinzugefügt werden. Bitte die Person, cordn zuerst auf diesem Koordinator zu öffnen.</string> + <string name="cordn_admin_no_self_remove">Du kannst dich nicht selbst aus einer cordn-Gruppe entfernen.</string> + <string name="cordn_admin_no_welcome">%1$s konnte kein Zugang zur Gruppe gegeben werden, daher wurde das Hinzufügen abgebrochen.</string> + <string name="cordn_admin_not_a_member">%1$s ist nicht in dieser Gruppe.</string> + <string name="cordn_admin_someone">Diese Person</string> + <string name="cordn_admin_wrong_key_package">Dieser Koordinator hat ein KeyPackage geliefert, das jemand anderem gehört, daher wurde %1$s nicht hinzugefügt. Es wurde nichts geändert.</string> + <string name="cordn_backup_contents_body">Deine Gruppen-Verschlüsselungsschlüssel, deine KeyPackages und jede Nachricht in diesen Gruppen. Wer die Datei öffnet, kann die Unterhaltungen lesen. Sobald sie Amethyst verlässt, schützt sie nur noch die Passphrase.</string> + <string name="cordn_backup_contents_title">Was in der Datei steckt</string> + <string name="cordn_backup_explainer">Dein nsec kann cordn-Gruppen nicht zurückholen. Eine cordn-Gruppe existiert als Verschlüsselungszustand auf diesem Gerät und als geordneter Datenstrom auf einem Koordinator, der ihn nicht lesen kann — verlierst du das Gerät ohne Backup, ist die Gruppe weg, samt allem, was darin schon gesagt wurde.</string> + <string name="cordn_backup_export">Exportieren</string> + <string name="cordn_backup_exported">Backup gespeichert.</string> + <string name="cordn_backup_failed">Das hat nicht funktioniert. Prüfe die Passphrase und die Datei.</string> + <string name="cordn_backup_passphrase">Passphrase</string> + <string name="cordn_backup_restore">Wiederherstellen</string> + <string name="cordn_backup_restore_body">Beim Wiederherstellen werden die cordn-Gruppen auf diesem Gerät durch die aus der Datei ersetzt. Das ist für ein Smartphone gedacht, das ein anderes abgelöst hat — nicht, um dieselben Gruppen gleichzeitig auf zwei Geräten zu nutzen.</string> + <string name="cordn_backup_restore_confirm_body">Jede cordn-Gruppe, die jetzt auf diesem Gerät ist, wird entfernt und durch die aus der Datei ersetzt.\n\nStelle nicht auf einem zweiten Smartphone wieder her, während das erste noch genutzt wird. Beide hätten dieselben Gruppenschlüssel, und sobald beide eine Nachricht gesendet haben, geht die Gruppe für alle kaputt — unbemerkt und ohne Weg zurück.</string> + <string name="cordn_backup_restore_confirm_title">Die cordn-Gruppen dieses Geräts ersetzen?</string> + <string name="cordn_backup_restore_title">Wiederherstellen</string> + <string name="cordn_backup_restored">Wiederhergestellt.</string> + <string name="cordn_chat_empty_description">Alles, was hier gesendet wird, ist für diese Gruppe verschlüsselt. Der Koordinator leitet es weiter, ohne es lesen zu können.</string> + <string name="cordn_chat_empty_title">Noch keine Nachrichten</string> + <string name="cordn_chat_unread_divider">Neue Nachrichten</string> + <string name="cordn_chat_yesterday">Gestern</string> + <string name="cordn_composer_hint">Nachricht</string> + <string name="cordn_coordinators_add_action">Hinzufügen</string> + <string name="cordn_coordinators_add_failed">Dieser Koordinator konnte nicht geöffnet werden.</string> + <string name="cordn_coordinators_copy_key">npub des Koordinators kopieren</string> + <string name="cordn_coordinators_discover_action">Nach Koordinatoren suchen</string> + <string name="cordn_coordinators_discover_add">Hinzufügen</string> + <string name="cordn_coordinators_discover_explainer">Liest Ankündigungen, die deine Relays ohnehin führen. Kein Koordinator wird kontaktiert, daher erfährt keiner, dass du gesucht hast.</string> + <string name="cordn_coordinators_discover_failed">Ankündigungen konnten nicht gelesen werden</string> + <string name="cordn_coordinators_discover_none">Auf deinen Relays kündigt sich niemand an.</string> + <string name="cordn_coordinators_discover_seen">Vor %1$s angekündigt</string> + <string name="cordn_coordinators_discover_seen_on">Zuletzt angekündigt %1$s</string> + <plurals name="cordn_coordinators_discover_unheard"> + <item quantity="one">Nichts gefunden, und %1$d deiner Relays hat nicht geantwortet.</item> + <item quantity="other">Nichts gefunden, und %1$d deiner Relays haben nicht geantwortet.</item> + </plurals> + <string name="cordn_coordinators_explainer">Ein Koordinator legt die Reihenfolge der Nachrichten einer Gruppe fest und ist der Einzige, der das kann. Geht er verloren, ist die Reihenfolge verloren, und ein zweiter spiegelt den ersten nicht — daher sind das benannte Dienste, die du auswählst, keine austauschbaren Relays.</string> + <plurals name="cordn_coordinators_health_down"> + <item quantity="one">Antwortet nicht — %1$d Anfrage in Folge ist fehlgeschlagen. Gruppen darauf werden nicht synchronisiert.</item> + <item quantity="other">Antwortet nicht — %1$d Anfragen in Folge sind fehlgeschlagen. Gruppen darauf werden nicht synchronisiert.</item> + </plurals> + <string name="cordn_coordinators_health_ok">Antwortet.</string> + <string name="cordn_coordinators_health_retrying">Eine Anfrage ist fehlgeschlagen; neuer Versuch läuft.</string> + <string name="cordn_coordinators_health_unknown">Noch nichts angefragt.</string> + <string name="cordn_coordinators_hide_older">Die ausblenden, die sich nicht mehr ankündigen</string> + <string name="cordn_coordinators_identify">Fragen, wer er ist</string> + <string name="cordn_coordinators_key">Koordinator-Schlüssel</string> + <string name="cordn_coordinators_label">Ein Name dafür (deiner, optional)</string> + <string name="cordn_coordinators_label_note">Dieser Name gehört dir und bleibt auf diesem Gerät. Ein Koordinator kann keinen Namen beweisen, daher wird er nie nach einem gefragt.</string> + <string name="cordn_coordinators_more">Weitere Aktionen</string> + <string name="cordn_coordinators_none">Noch keine Koordinatoren.</string> + <string name="cordn_coordinators_purge">Löschen</string> + <string name="cordn_coordinators_purge_body">Beim Entfernen bleiben die Gruppen auf diesem Gerät, sodass sie zurückkommen, wenn du den Koordinator wieder hinzufügst. Beim Löschen werden sie vernichtet: Die Gruppenschlüssel, die Lesepositionen und deine KeyPackages verschwinden, und diese Gruppen können nur über eine neue Einladung wieder betreten werden.\n\nDer Koordinator wird nicht benachrichtigt und behält alles, was er bereits hatte.</string> + <string name="cordn_coordinators_purge_title">Diesen Koordinator löschen?</string> + <string name="cordn_coordinators_relays">%1$s</string> + <string name="cordn_coordinators_relays_label">Antwortet auf</string> + <string name="cordn_coordinators_relays_more">%1$s +%2$d weitere</string> + <string name="cordn_coordinators_remove">Entfernen</string> + <string name="cordn_coordinators_rename">Umbenennen</string> + <string name="cordn_coordinators_server">Gibt an: %1$s</string> + <string name="cordn_coordinators_server_claim">Name und Version eines Koordinators sind selbst gewählte Angaben. Nur sein öffentlicher Schlüssel identifiziert ihn.</string> + <string name="cordn_coordinators_server_silent">Hat geantwortet, aber nichts angegeben.</string> + <string name="cordn_coordinators_show_all">Alle %1$d anzeigen</string> + <string name="cordn_coordinators_show_fewer">Weniger anzeigen</string> + <plurals name="cordn_coordinators_show_older"> + <item quantity="one">%1$d anzeigen, der sich nicht mehr ankündigt</item> + <item quantity="other">%1$d anzeigen, die sich nicht mehr ankündigen</item> + </plurals> + <string name="cordn_coordinators_stale_note">Diese haben sich seit über einem Monat nicht angekündigt. Eine Gruppe auf einem verschwundenen Koordinator zu erstellen, schlägt fehl.</string> + <string name="cordn_create_action">Gruppe erstellen</string> + <plurals name="cordn_create_action_invite"> + <item quantity="one">Erstellen und %1$d Person einladen</item> + <item quantity="other">Erstellen und %1$d Personen einladen</item> + </plurals> + <string name="cordn_create_add_member">Jemanden hinzufügen</string> + <string name="cordn_create_admin">Admin</string> + <plurals name="cordn_create_admin_count"> + <item quantity="one">%1$d Person kann hinzufügen und entfernen</item> + <item quantity="other">%1$d Personen können hinzufügen und entfernen</item> + </plurals> + <string name="cordn_create_admin_only_me">Nur ich verwalte diese Gruppe</string> + <string name="cordn_create_admin_only_me_on">Nur du kannst einladen, Mitglieder entfernen oder die Gruppendetails ändern. Du kannst das später an jemand anderen übergeben.</string> + <string name="cordn_create_coordinator">Koordinator</string> + <string name="cordn_create_coordinator_change">Ändern</string> + <string name="cordn_create_coordinator_discover">Koordinatoren finden</string> + <string name="cordn_create_coordinator_hide">Fertig</string> + <string name="cordn_create_coordinator_new">Anderer Koordinator</string> + <string name="cordn_create_coordinator_pubkey">Öffentlicher Schlüssel des Koordinators (hex oder npub)</string> + <string name="cordn_create_coordinator_relays">Relays, auf denen er antwortet, eines pro Zeile</string> + <plurals name="cordn_create_coverage_all"> + <item quantity="one">Erreicht deine %1$d Person</item> + <item quantity="other">Erreicht alle %1$d deiner Personen</item> + </plurals> + <string name="cordn_create_coverage_partial">Erreicht %1$d von %2$d — die übrigen blieben außen vor</string> + <string name="cordn_create_coverage_unknown">Nicht geprüft — bei Auswahl wird nachgefragt</string> + <string name="cordn_create_description">Beschreibung (optional)</string> + <string name="cordn_create_egalitarian">Alle können hinzufügen und entfernen</string> + <string name="cordn_create_egalitarian_note">Alle in der Gruppe können einladen, Mitglieder entfernen und die Details ändern. Jedes Mitglied kann später Admins ernennen, womit das endet.</string> + <string name="cordn_create_explainer">Eine cordn-Gruppe wird von einem Koordinator deiner Wahl geordnet. Die Gruppe wird auf diesem Gerät erstellt — der Koordinator erfährt erst von ihr, wenn du jemanden einlädst oder eine Nachricht sendest.</string> + <string name="cordn_create_failed">Die Gruppe konnte nicht erstellt werden.</string> + <string name="cordn_create_name">Gruppenname</string> + <string name="cordn_create_no_coordinator">Kein Koordinator ausgewählt</string> + <string name="cordn_create_outcome_explainer">Jede Einladung ist eine Welcome-Nachricht, die beim Koordinator hinterlegt wird, daher können sie einzeln fehlschlagen. Niemand wurde benachrichtigt: Die Eingeladenen sehen sie, wenn sie ihre App das nächste Mal öffnen.</string> + <string name="cordn_create_outcome_failed">Der Koordinator hat die Einladung abgelehnt</string> + <string name="cordn_create_outcome_link">Link senden</string> + <string name="cordn_create_outcome_no_welcome">Zur Gruppe hinzugefügt, aber die Einladung hat die Person nicht erreicht — versuche es auf dem Gruppenbildschirm erneut</string> + <string name="cordn_create_outcome_open">Chat öffnen</string> + <string name="cordn_create_outcome_skipped">Kein Schlüssel hier, daher konnte keine Welcome-Nachricht hinterlegt werden</string> + <string name="cordn_create_outcome_title">Die Gruppe existiert</string> + <string name="cordn_create_outcome_waiting">Warte auf die Person</string> + <string name="cordn_create_people_done">Fertig</string> + <string name="cordn_create_people_empty">Noch niemand hinzugefügt. Eine Gruppe kann leer erstellt und Einladungen später gesendet werden, aber wenn du jetzt Personen nennst, zeigt sich, welcher Koordinator sie erreichen kann.</string> + <string name="cordn_create_people_none">Noch niemand — tippen, um Personen hinzuzufügen</string> + <string name="cordn_create_people_note">Wenn du jetzt Personen nennst, erfahren wir, welcher Koordinator sie erreichen kann. Jede erhält eine Welcome-Nachricht auf diesem Koordinator, die sie abholt, wenn sie ihre App das nächste Mal öffnet.</string> + <plurals name="cordn_create_reach_count"> + <item quantity="one">Erreichbar über %1$d Koordinator</item> + <item quantity="other">Erreichbar über %1$d Koordinatoren</item> + </plurals> + <string name="cordn_create_reach_none">Kein Schlüssel auf einem deiner Koordinatoren</string> + <string name="cordn_create_reach_unknown">Noch nicht geprüft</string> + <string name="cordn_create_step_admins">Wer hinzufügen und entfernen kann</string> + <string name="cordn_create_step_name">Name</string> + <string name="cordn_create_step_people">Wer dabei ist</string> + <string name="cordn_create_step_where">Wo sie liegt</string> + <string name="cordn_create_title">Neue Cordn-Gruppe</string> + <plurals name="cordn_create_unreachable_count"> + <item quantity="one">%1$d Person ist über keinen deiner Koordinatoren erreichbar</item> + <item quantity="other">%1$d Personen sind über keinen deiner Koordinatoren erreichbar</item> + </plurals> + <string name="cordn_create_you">Du, immer Admin</string> + <string name="cordn_delete_confirm_body">Alle in der Gruppe sehen diese Nachricht als zurückgezogen, und Amethyst zeigt ihren Text nicht mehr an. Der Koordinator behält die verschlüsselte Kopie, als die sie gesendet wurde, und wer sie schon gelesen hat, hat sie gelesen.</string> + <string name="cordn_delete_confirm_title">Nachricht zurückziehen</string> + <string name="cordn_delivery_accepted">Vom Koordinator angenommen</string> + <string name="cordn_exposure_close">Verstanden</string> + <string name="cordn_exposure_open">Was dieser Koordinator sehen kann</string> + <string name="cordn_group_info">Gruppeninfo</string> + <string name="cordn_group_unavailable">Diese Gruppe ist nicht verfügbar. Ihr Koordinator wurde möglicherweise entfernt.</string> + <string name="cordn_group_unavailable_detail">Füge in den Einstellungen einen Koordinator hinzu, um cordn-Gruppen zu nutzen.</string> + <plurals name="cordn_groups_count"> + <item quantity="one">%1$d Gruppe</item> + <item quantity="other">%1$d Gruppen</item> + </plurals> + <string name="cordn_groups_down_note">Aus diesen Gruppen kommt nichts an. Amethyst versucht es weiter.</string> + <string name="cordn_groups_manage">Koordinatoren</string> + <string name="cordn_groups_none">Noch keine cordn-Gruppen</string> + <string name="cordn_groups_none_detail">Eine cordn-Gruppe liegt auf einem Koordinator, der ihre Mitgliedschaft verwaltet und Nachrichten weitergibt, ohne je zu sehen, was jemand schreibt.</string> + <string name="cordn_groups_start">Eine starten</string> + <string name="cordn_info_add_member">Jemanden hinzufügen</string> + <string name="cordn_info_add_member_none">Niemand mit diesem Namen gefunden.</string> + <string name="cordn_info_add_member_note">Personen können nur hinzugefügt werden, wenn sie ein KeyPackage bei diesem Koordinator veröffentlicht haben.</string> + <string name="cordn_info_add_member_placeholder">Name, npub oder name@domain</string> + <string name="cordn_info_admin_badge">Admin</string> + <plurals name="cordn_info_admins_kept"> + <item quantity="one">Der %1$d Admin der Gruppe bleibt unverändert.</item> + <item quantity="other">Die %1$d Admins der Gruppe bleiben unverändert.</item> + </plurals> + <string name="cordn_info_coordinator">Koordinator</string> + <string name="cordn_info_coordinator_key">Koordinator-Schlüssel</string> + <string name="cordn_info_coordinator_nprofile">Koordinator-Link</string> + <string name="cordn_info_copy_nprofile">nprofile des Koordinators kopieren</string> + <string name="cordn_info_edit_details">Details bearbeiten</string> + <string name="cordn_info_egalitarian">Diese Gruppe hat keine Admins: Alle können dauerhaft hinzufügen, entfernen und umbenennen.</string> + <string name="cordn_info_epoch">Epoche</string> + <string name="cordn_info_gid">Gruppen-ID</string> + <string name="cordn_info_has_key_package">Kann über diesen Koordinator hinzugefügt werden</string> + <string name="cordn_info_members">Mitglieder</string> + <string name="cordn_info_remove_confirm_body">%1$s erhält von hier keine Nachrichten mehr. Bereits Gelesenes bleibt erhalten, und das lässt sich nicht rückgängig machen — die Person müsste erneut eingeladen werden.</string> + <string name="cordn_info_remove_confirm_title">Aus der Gruppe entfernen?</string> + <string name="cordn_info_remove_member">Entfernen</string> + <string name="cordn_info_save">Speichern</string> + <string name="cordn_info_technical">Technische Details</string> + <string name="cordn_invitations_accept">Beitreten</string> + <string name="cordn_invitations_decline">Ablehnen</string> + <string name="cordn_invitations_decline_warning">Ablehnen ist endgültig. Um wieder hineinzukommen, musst du erneut eingeladen werden.</string> + <string name="cordn_invitations_explainer">Wird nur geprüft, wenn du diesen Bildschirm öffnest, und sonst nie — einen Koordinator zu fragen, ob dich jemand eingeladen hat, verrät ihm, dass du da bist, daher tut Amethyst das nicht im Hintergrund.</string> + <string name="cordn_invitations_load_failed">Einladungen konnten nicht gelesen werden.</string> + <plurals name="cordn_invitations_members"> + <item quantity="one">%1$d Person ist bereits in dieser Gruppe</item> + <item quantity="other">%1$d Personen sind bereits in dieser Gruppe</item> + </plurals> + <string name="cordn_invitations_members_more">+%1$d weitere</string> + <string name="cordn_invitations_no_coordinators">Du hast keinen Koordinator geöffnet. cordn-Einladungen kommen über einen Koordinator an, daher gibt es noch nirgends etwas nachzusehen.</string> + <string name="cordn_invitations_none">Keine wartenden Einladungen.</string> + <string name="cordn_invitations_refresh">Erneut prüfen</string> + <string name="cordn_invitations_skipped">Eine Einladung wurde für ein anderes Gerät hinterlegt</string> + <string name="cordn_invitations_title">Cordn-Einladungen</string> + <string name="cordn_invitations_unreachable">%1$s hat nicht geantwortet</string> + <string name="cordn_invitations_via">Über %1$s</string> + <string name="cordn_keypackages_disclosure">Beim Veröffentlichen wird ein Eintrag mit deinem eigenen Kontoschlüssel signiert. Dieser Koordinator weiß dann, dass dieses Konto existiert und eingeladen werden kann — dauerhaft, und unabhängig davon, ob dich jemand einlädt.</string> + <string name="cordn_keypackages_explainer">Andere können dich nur zu einer cordn-Gruppe hinzufügen, indem sie ein KeyPackage verwenden, das du bei diesem Koordinator veröffentlicht hast. cordn hat keinen anderen Ort dafür, also kann dich ohne veröffentlichtes KeyPackage niemand einladen — und nichts sagt ihnen, warum.</string> + <string name="cordn_keypackages_failed">Die KeyPackages konnten nicht gelesen werden.</string> + <string name="cordn_keypackages_last_resort_no">Kein Last-Resort-Paket. Sobald die Einmal-Pakete aufgebraucht sind, schlagen Einladungen fehl.</string> + <string name="cordn_keypackages_last_resort_yes">Ein Last-Resort-Paket ist veröffentlicht, daher sind Einladungen auch dann noch möglich, wenn die Einmal-Pakete aufgebraucht sind.</string> + <plurals name="cordn_keypackages_orphans"> + <item quantity="one">%1$d Paket, das dieses Gerät nicht öffnen kann</item> + <item quantity="other">%1$d Pakete, die dieses Gerät nicht öffnen kann</item> + </plurals> + <string name="cordn_keypackages_orphans_body">Der Koordinator gibt diese an jeden weiter, der dich einlädt, aber die private Hälfte ist nicht auf diesem Gerät — sie gehört zu einer anderen Installation. Gibt es diese Installation nicht mehr, erzeugt eine Einladung mit einem dieser Pakete eine Welcome-Nachricht, die niemand je öffnen kann.</string> + <string name="cordn_keypackages_publish">Eines veröffentlichen</string> + <string name="cordn_keypackages_publish_last_resort">Last-Resort veröffentlichen</string> + <plurals name="cordn_keypackages_summary"> + <item quantity="one">%1$d Einmal-Paket verfügbar.</item> + <item quantity="other">%1$d Einmal-Pakete verfügbar.</item> + </plurals> + <string name="cordn_keypackages_topup_note">Sobald du hier veröffentlicht hast, ersetzt Amethyst verbrauchte Pakete unauffällig. Das erste veröffentlicht es nie für dich.</string> + <string name="cordn_keypackages_withdraw_all">Alle zurückziehen</string> + <string name="cordn_keypackages_withdraw_orphans">Diese zurückziehen</string> + <string name="cordn_link_request">Beitritt zu dieser Gruppe anfragen</string> + <string name="cordn_link_request_disclosure">Dies veröffentlicht ein KeyPackage unter deinem eigenen Kontoschlüssel und teilt dem Koordinator mit, dass du in diese Gruppe möchtest — unabhängig davon, ob jemand antwortet.</string> + <string name="cordn_link_request_failed">Beitrittsanfrage fehlgeschlagen.</string> + <string name="cordn_link_request_sent">Angefragt. Ein Mitglied der Gruppe muss dich hinzufügen; die Einladung erscheint unter cordn-Einladungen.</string> + <string name="cordn_link_scan">Scannen</string> + <string name="cordn_media_download_failed">Diese Datei konnte nicht geöffnet werden.</string> + <string name="cordn_media_no_server">Für dieses Konto ist kein Medienserver festgelegt, daher gibt es keinen Ablageort für die Datei. Wähle einen in den Einstellungen.</string> + <string name="cordn_media_unreadable">Diese Datei konnte nicht gelesen werden.</string> + <string name="cordn_media_upload_failed">Diese Datei konnte nicht gesendet werden.</string> + <plurals name="cordn_member_count"> + <item quantity="one">%1$d Mitglied</item> + <item quantity="other">%1$d Mitglieder</item> + </plurals> + <string name="cordn_message_deleted">Nachricht gelöscht</string> + <string name="cordn_message_details_coordinator">Koordinator</string> + <string name="cordn_message_details_cursor">Cursor</string> + <string name="cordn_message_details_sent_at">Gesendet</string> + <string name="cordn_message_details_title">Nachrichtendetails</string> + <string name="cordn_message_edited">bearbeitet</string> + <string name="cordn_pinned_by">Angeheftet von %1$s</string> + <plurals name="cordn_pinned_count"> + <item quantity="one">%1$d angeheftete Nachricht in dieser Gruppe</item> + <item quantity="other">%1$d angeheftete Nachrichten in dieser Gruppe</item> + </plurals> + <string name="cordn_pinned_next">Nächste angeheftete Nachricht</string> + <string name="cordn_pinned_previous">Vorherige angeheftete Nachricht</string> + <string name="cordn_pinned_show_all">Alle angehefteten Nachrichten anzeigen</string> + <string name="cordn_pinned_title">Angeheftete Nachrichten</string> + <string name="cordn_reactions_title">Reaktionen</string> + <string name="cordn_requests_accept">Hinzufügen</string> + <string name="cordn_requests_admin_only">Nur ein Admin kann annehmen. In einer Gruppe ohne Admins sind das alle.</string> + <string name="cordn_requests_check">Nach Anfragen suchen</string> + <string name="cordn_requests_decline">Verwerfen</string> + <string name="cordn_requests_failed">Die Anfragen konnten nicht gelesen werden.</string> + <string name="cordn_requests_none">Niemand wartet auf den Beitritt.</string> + <string name="cordn_requests_title">Beitrittsanfragen</string> + <string name="cordn_send">Senden</string> + <string name="cordn_send_failed">Das wurde nicht gesendet.</string> + <string name="cordn_send_no_session">Der Koordinator dieser Gruppe ist nicht geöffnet, daher kann noch nichts gesendet werden.</string> + <string name="cordn_share_copied">Kopiert</string> + <string name="cordn_share_copy">Link kopieren</string> + <string name="cordn_share_explainer">Jeder mit diesem Link kann den Beitritt anfragen. Ein Admin muss ihn trotzdem noch hereinlassen.</string> + <string name="cordn_share_title">Diese Gruppe teilen</string> + <string name="cordn_voice_play">Sprachnachricht abspielen</string> + <string name="cordn_voice_record">Sprachnachricht aufnehmen</string> + <string name="cordn_voice_stop">Stoppen und senden</string> + <string name="qr_scanner_camera_rationale">Amethyst braucht die Kamera, um QR-Codes zu scannen. Es wird nichts aufgezeichnet oder hochgeladen — die Bilder werden auf dem Gerät ausgewertet und verworfen.</string> + <string name="qr_scanner_grant_camera">Kamera erlauben</string> + <string name="qr_scanner_camera_blocked">Der Kamerazugriff ist für Amethyst deaktiviert. Du kannst ihn in den Android-Einstellungen wieder aktivieren.</string> + <string name="qr_scanner_open_settings">Einstellungen öffnen</string> + <string name="qr_scanner_unavailable">Der QR-Decoder konnte auf diesem Gerät nicht starten. Du kannst den Code trotzdem als Text einfügen.</string> + <string name="qr_scanner_torch_on">Licht einschalten</string> + <string name="qr_scanner_torch_off">Licht ausschalten</string> + <string name="qr_scanner_dark_hint">Es ist dunkel — probier das Licht</string> + <string name="qr_scanner_scan_image">Bild scannen</string> + <string name="qr_scanner_paste">Einfügen</string> + <string name="qr_scanner_no_code_in_image">Kein QR-Code in diesem Bild gefunden</string> + <string name="qr_scanner_clipboard_empty">Nichts zum Scannen in der Zwischenablage</string> + <string name="qr_scanner_pick_one">Tippe auf den gewünschten Code</string> + <plurals name="qr_scanner_sequence_progress"> + <item quantity="one">%1$s von %2$s Teil erfasst — weiter so</item> + <item quantity="other">%1$s von %2$s Teilen erfasst — weiter so</item> + </plurals> + <string name="qr_scanner_zoom_reset">Zoom zurücksetzen</string> + <string name="qr_scanner_unsupported_title">Amethyst kann diesen Code nicht öffnen</string> + <string name="qr_scanner_unsupported_secret">Dieser Code enthält einen privaten Schlüssel oder ein Kopplungsgeheimnis, daher wird sein Inhalt hier nicht angezeigt. Verwende den Bildschirm, der ihn erwartet.</string> + <string name="qr_scanner_kind_nsec">Das ist ein privater Schlüssel. Füge ihn stattdessen auf dem Anmeldebildschirm ein.</string> + <string name="qr_scanner_kind_wallet">Das ist eine Wallet-Verbindung. Füge sie in den Wallet-Einstellungen hinzu.</string> + <string name="qr_scanner_kind_bunker">Das ist eine Remote-Signer-Adresse. Amethyst gibt solche Adressen heraus, damit andere Apps es um Signaturen bitten können — anmelden kann es sich damit nicht.</string> + <string name="qr_scanner_kind_signer">Das ist eine App, die sich mit deinem Signierer verbinden möchte. Öffne sie über den NIP-46-Signierer-Bildschirm.</string> + <string name="qr_scanner_kind_lightning">Das ist eine Lightning-Rechnung.</string> + <string name="qr_scanner_kind_cashu">Das ist ein Cashu-Token.</string> + <string name="qr_scanner_kind_web">Das ist ein Weblink.</string> + <string name="qr_scanner_kind_text">Das ist einfacher Text, kein Nostr-Code.</string> + <string name="qr_scanner_kind_nostr_unsupported">Das ist ein Nostr-Code, aber keiner, den dieser Bildschirm öffnen kann.</string> + <string name="qr_scanner_label_profile">Profil</string> + <string name="qr_scanner_label_note">Beitrag</string> + <string name="qr_scanner_label_nostr">Nostr-Code</string> + <string name="qr_scanner_label_relay">Relay</string> + <string name="qr_scanner_label_nsec">Privater Schlüssel</string> + <string name="qr_scanner_label_wallet">Wallet-Verbindung</string> + <string name="qr_scanner_label_bunker">Remote-Signer-Adresse</string> + <string name="qr_scanner_label_signer">Verbindungsanfrage an Signierer</string> + <string name="qr_scanner_label_lightning">Lightning-Rechnung</string> + <string name="qr_scanner_label_cashu">Cashu-Token</string> + <string name="qr_scanner_label_web">Weblink</string> + <string name="qr_scanner_label_text">Einfacher Text</string> + <string name="qr_scanner_open_link">Link öffnen</string> + <string name="qr_scanner_copy">Kopieren</string> + <string name="qr_scanner_copied">Kopiert</string> + <string name="qr_scanner_try_again">Erneut scannen</string> + <string name="workout_suggestion_connect_title">Nutze die Trainings, die du schon aufgezeichnet hast</string> + <string name="workout_suggestion_connect_message">Verbinde Health Connect (Samsung Health, Google Fit, Fitbit, Garmin…), um „Meine Fitness“ aufzubauen, deine persönliche Trainingsübersicht, und um dieses Training aus einem zu übernehmen, das deine Uhr bereits aufgezeichnet hat, statt jede Zahl von Hand einzutippen.</string> + <string name="health_connect_rationale_headline">Amethyst nutzt Health Connect, um „Meine Fitness“ aufzubauen — deine persönliche Trainingsübersicht, auf deinem Telefon.</string> + <string name="health_connect_rationale_intro">„Meine Fitness“ (Menü → Du → Meine Fitness) zeigt dir, wie du trainiert hast: wie viel du diese Woche im Vergleich zur letzten gemacht hast, wie sich deine Zeit auf Laufen, Radfahren, Gehen, Schwimmen und Fitnessstudio verteilt, deine Bestleistungen, an wie vielen Tagen du trainiert hast und deine aktuelle Serie. Amethyst erstellt dieses Bild aus den Trainings, die deine Uhr oder Fitness-App bereits in Health Connect gespeichert hat, zusammen mit allen Trainings, die du in Amethyst selbst erfasst hast. Es wird auf deinem Telefon berechnet und nur dir angezeigt.</string> + <string name="health_connect_rationale_exercise">Training · welche Aktivität du gemacht hast, wann sie begann und wie lange sie dauerte. Das ist das Training selbst: Daraus ergeben sich deine Anzahl an Trainings, deine Trainingszeit, die Aufschlüsselung nach Aktivität, deine aktiven Tage und deine Serie.</string> + <string name="health_connect_rationale_distance">Distanz · wie weit du gekommen bist, angezeigt als deine Distanz dieser Woche im Vergleich zur letzten, dein Wochendurchschnitt, deine Distanz pro Aktivität und deine längste Distanz.</string> + <string name="health_connect_rationale_calories">Aktive und Gesamtkalorien · die Energie, die ein Training verbrannt hat, angezeigt als deine Energie dieser Woche und im Vergleich zur letzten Woche. Aktive Kalorien werden verwendet, wenn deine Quelle sie erfasst; Gesamtkalorien dienen als Ersatz für Quellen, die nur den Gesamtenergieverbrauch erfassen.</string> + <string name="health_connect_rationale_heart_rate">Herzfrequenz · Durchschnitt und Maximum jedes Trainings, zusammengefasst zu deiner durchschnittlichen und höchsten Herzfrequenz im Zeitraum. So unterscheidest du eine lockere Woche von einer harten bei gleicher Distanz.</string> + <string name="health_connect_rationale_steps">Schritte · die Schrittzahl eines Laufs, Spaziergangs oder einer Wanderung, angezeigt als dein wöchentlicher Schrittdurchschnitt und deine höchste Schrittzahl.</string> + <string name="health_connect_rationale_elevation">Höhenmeter · wie viel du aufgestiegen bist, angezeigt als dein wöchentlicher Durchschnitt an Höhenmetern und dein größter Anstieg. Daran erkennst du eine flache Woche von einer hügeligen.</string> + <string name="health_connect_rationale_limit_window">Liest nur Trainings der letzten 4 Wochen, und nur solange „Meine Fitness“ oder der Trainings-Editor geöffnet ist. Liest nie im Hintergrund.</string> + <string name="health_connect_rationale_limit_publish">Sendet deine Gesundheitsdaten nirgendwohin. „Meine Fitness“ wird auf deinem Telefon berechnet und bleibt dort. Wenn du später ein Training posten möchtest, tippst du bei diesem Training auf Teilen, liest den Beitrag, den Amethyst ausgefüllt hat, und veröffentlichst ihn selbst — ein Training nach dem anderen, nie automatisch.</string> + <string name="health_connect_rationale_limit_optional">Die gesamte Funktion ist optional. Widerrufe die Berechtigungen jederzeit in Health Connect — „Meine Fitness“ zeigt dann wieder die Aufforderung — oder schalte die Editor-Vorschläge unter Einstellungen → Editor-Einstellungen aus. Der Rest von Amethyst funktioniert weiter.</string> + <string name="my_fitness_title">Meine Fitness</string> + <string name="my_fitness_window">Letzte 4 Wochen</string> + <string name="my_fitness_loading_metrics">Distanz, Kalorien und Herzfrequenz werden geladen…</string> + <string name="my_fitness_window_note">Die letzten vier vollen Wochen deines Trainings: die Trainings, die du veröffentlicht hast, plus alles, was Health Connect auf diesem Telefon erfasst. Amethyst berechnet die Übersicht auf dem Gerät und veröffentlicht nichts davon.</string> + <string name="my_fitness_this_week">Diese Woche</string> + <string name="my_fitness_vs_last_week">ggü. letzter Woche</string> + <string name="my_fitness_weekly_average">Wochendurchschnitt</string> + <string name="my_fitness_workouts">Trainings</string> + <string name="my_fitness_active_days">Aktive Tage</string> + <string name="my_fitness_streak">Tage in Folge</string> + <string name="my_fitness_time">Zeit</string> + <string name="my_fitness_duration_hours_minutes">%1$d Std. %2$d Min.</string> + <string name="my_fitness_duration_minutes">%1$d Min.</string> + <string name="my_fitness_duration_seconds">%1$d Sek.</string> + <string name="my_fitness_distance">Distanz</string> + <string name="my_fitness_calories">Kalorien</string> + <string name="my_fitness_steps">Schritte</string> + <string name="my_fitness_elevation">Höhenmeter</string> + <string name="my_fitness_avg_heart_rate">Ø HF</string> + <string name="my_fitness_max_heart_rate">Max. HF</string> + <string name="my_fitness_by_activity">Nach Aktivität</string> + <string name="my_fitness_bests">Bestleistungen</string> + <string name="my_fitness_best_longest_distance">Längste Distanz</string> + <string name="my_fitness_best_longest_duration">Längstes Training</string> + <string name="my_fitness_best_biggest_climb">Größter Anstieg</string> + <string name="my_fitness_best_most_steps">Meiste Schritte</string> + <string name="my_fitness_best_highest_heart_rate">Höchste Herzfrequenz</string> + <string name="my_fitness_recent">Letzte Trainings</string> + <string name="my_fitness_empty">In den letzten 4 Wochen wurde nichts erfasst. Trainings, die du postest, und alle, die deine Uhr aufzeichnet, erscheinen hier.</string> + <string name="my_fitness_connect_title">Verfolge dein Training</string> + <string name="my_fitness_connect_message">Verbinde Health Connect, um deine Wochensummen, den Trend im Wochenvergleich, deine Bestleistungen und deine Trainingsserie zu sehen. Alles bleibt auf deinem Telefon.</string> + <string name="my_fitness_connect_banner">Verbinde Health Connect, um die Trainings deiner Uhr mit Herzfrequenz, Schritten und Höhenmetern hinzuzufügen.</string> + <string name="my_fitness_connect_button">Verbinden</string> + <string name="my_fitness_share">Dieses Training teilen</string> + <string name="my_fitness_unit_bpm">S/min</string> + <string name="my_fitness_unit_kcal">kcal</string> + <string name="my_fitness_unit_km">km</string> + <string name="my_fitness_unit_mi">mi</string> + <string name="my_fitness_unit_m">m</string> + <string name="my_fitness_unit_ft">ft</string> + <string name="looking_for_event_title">Wird geladen</string> + <string name="looking_for_private_event">Warte, bis deine privaten Nachrichten geladen sind…</string> + <string name="call_duration_minutes_seconds">%1$02d:%2$02d</string> + <string name="call_duration_hours_minutes_seconds">%1$d:%2$02d:%3$02d</string> + <string name="marmot_preview_group_updated">Gruppe aktualisiert</string> + <string name="marmot_preview_media">Anhang</string> + <string name="marmot_preview_no_text">Nachricht</string> + <string name="marmot_preview_with_sender">%1$s: %2$s</string> + <string name="marmot_empty_create_action">Gruppe erstellen</string> + <string name="marmot_invite_device_banner">Marmot-Einladungen gehen an ein anderes Gerät und können hier nicht geöffnet werden.</string> + <string name="marmot_invite_device_banner_dismiss">Schließen</string> + <string name="marmot_invite_device_publish">Von diesem Gerät veröffentlichen</string> + <string name="wallet_open_zapped_note">Gezappten Beitrag öffnen</string> + <string name="video_player_settings_action_captions">Untertitel</string> + <string name="video_player_settings_action_captions_description">Untertitel ein- oder ausschalten (ausgeblendet, wenn das Video keine hat)</string> + <string name="app_notification_reply_failed">Nicht gesendet</string> + <string name="app_notification_reply_retry">Erneut versuchen</string> + <string name="app_notification_reply_sending">Wird gesendet…</string> + <string name="app_notification_reply_unconfirmed">Wird noch gesendet…</string> + <string name="app_notification_shortcut_account_removed">Dieses Konto wurde aus Amethyst entfernt.</string> + <string name="captions_off">Aus</string> + <string name="captions_turn_off">Untertitel ausschalten</string> + <string name="captions_turn_on">Untertitel einschalten</string> + <string name="captions_unnamed_track">Untertitel</string> + <string name="kind_app_curation_set">App-Liste</string> + <string name="kind_article_curation_set">Artikelliste</string> + <string name="kind_bookmark_set">Lesezeichen-Set</string> + <string name="kind_favorite_algo_feeds">Lieblings-Feeds</string> + <string name="kind_git_authors">Git-Autoren</string> + <string name="kind_git_repositories">Git-Repositories</string> + <string name="kind_good_wiki_authors">Vertrauenswürdige Wiki-Autoren</string> + <string name="kind_good_wiki_relays">Vertrauenswürdige Wiki-Relays</string> + <string name="kind_interest_set">Interessen</string> + <string name="kind_media_follows">Medien-Follows</string> + <string name="kind_media_starter_pack">Medien-Starterpaket</string> + <string name="kind_mute_set_for_kind">Stummgeschaltet für Typ %1$s</string> + <string name="kind_picture_curation_set">Bilderliste</string> + <string name="kind_simple_groups">Gruppen</string> + <string name="kind_video_collaboration">Video-Zusammenarbeit</string> + <string name="kind_video_list">Videoliste</string> + <string name="kind_video_subtitles">Video-Untertitel</string> + <string name="login_bunker_not_supported">Amethyst für Android kann sich nicht mit einer bunker://-Adresse anmelden. Es gibt solche Adressen heraus, damit andere Apps es um Signaturen bitten können, aber anmelden kann es sich damit nicht. Verwende eine externe Signierer-App oder Amethyst Desktop.</string> + <string name="login_nostrconnect_not_supported">Das ist eine App, die sich mit deinem Signierer verbinden möchte, keine Anmeldemöglichkeit. Melde dich zuerst an und öffne sie dann über den NIP-46-Signierer-Bildschirm.</string> + <string name="my_fitness">Meine Fitness</string> + <string name="nip51_list_has_private_members">Diese Liste hat private Mitglieder, die nur ihr Autor sehen kann</string> + <string name="route_edit_geocache">Cache bearbeiten</string> + <string name="route_edit_geocache_hunt">Tour bearbeiten</string> + <string name="route_geocache_detail">Cache</string> + <string name="route_geocache_hunt_detail">Tour</string> + <string name="route_geocache_hunts">Cache-Touren</string> + <string name="route_geocaches">Geocaches</string> + <string name="route_log_geocache_find">Fund loggen</string> + <string name="route_new_geocache">Cache verstecken</string> + <string name="route_new_geocache_hunt">Neue Tour</string> + <string name="video_collaboration_accepted_on">Hat eine Mitwirkungsnennung angenommen</string> + <string name="video_collaboration_declined_on">Hat eine Mitwirkungsnennung abgelehnt</string> + <string name="video_credit_accepted">Nennung angenommen</string> + <string name="video_credit_audio_from">Audio von</string> + <string name="video_credit_featuring">Mit</string> + <string name="video_credit_inspired_by">Inspiriert von</string> + <string name="video_credit_references">Verweise</string> + <string name="video_curation_set_private_items">Private Liste</string> + <plurals name="video_curation_set_video_count"> + <item quantity="one">%1$d Video</item> + <item quantity="other">%1$d Videos</item> + </plurals> + <string name="marmot_invite_device_success">Dieses Gerät empfängt jetzt deine Marmot-Einladungen.</string> + <string name="marmot_invite_device_failure">KeyPackage konnte nicht veröffentlicht werden: %1$s</string> + <string name="marmot_invite_device_rejected">Kein Relay hat das KeyPackage angenommen. Prüfe deine Relay-Einstellungen und versuche es erneut.</string> + <string name="chat_delivery_sending">Wird gesendet…</string> + <string name="chat_delivery_failed">Konnte nicht gesendet werden. Tippe, um es erneut zu versuchen.</string> + <plurals name="marmot_unread_messages"> + <item quantity="one">%1$d neue Nachricht</item> + <item quantity="other">%1$d neue Nachrichten</item> + </plurals> + <string name="chat_type_cordn_title">Cordn-Gruppen</string> + <string name="chat_type_cordn_desc">Ende-zu-Ende-verschlüsselte Gruppen, die von einem Koordinator deiner Wahl statt von Relays geordnet werden.</string> + <string name="new_conversation_cordn_title">Cordn-Gruppe</string> + <string name="new_conversation_cordn_tagline">Verschlüsselter Gruppenchat, geordnet von einem Koordinator deiner Wahl.</string> + <string name="new_conversation_cordn_chip">Koordiniert</string> + <string name="new_conversation_cordn_best">Ideal für Teams, die eine verlässliche Reihenfolge der Unterhaltung wollen.</string> + <string name="new_conversation_cordn_cta">Cordn-Gruppe erstellen</string> + <string name="new_conversation_cordn_pro_1">Ende-zu-Ende-verschlüsselt mit MLS — der Koordinator kann nie eine Nachricht lesen.</string> + <string name="new_conversation_cordn_pro_2">Eine gemeinsame Reihenfolge für alle, sodass der Verlauf nicht durch eine falsch gehende Uhr durcheinandergeraten kann.</string> + <string name="new_conversation_cordn_con_1">Der Koordinator erfährt, wer in der Gruppe ist und wann ihr schreibt, auch wenn er nicht lesen kann, was ihr sagt.</string> + <string name="sno_object_title">3D-Objekt</string> + <string name="sno_object_details">%1$d Eckpunkte · %2$d Flächen</string> + <string name="sno_object_unreadable">Dieses 3D-Objekt konnte nicht gelesen werden (Regel %1$s)</string> + <string name="sno_avatar_title">Cyberspace-Avatar</string> + <string name="sno_avatar_unpaid">Dieser Avatar hat nicht für seine Größe bezahlt und wird daher nicht gezeichnet</string> + <string name="sno_light_on">Beleuchtet; tippen, um das Objekt flach zu zeichnen</string> + <string name="sno_light_off">Flach; tippen, um das Objekt zu beleuchten</string> + <string name="sno_object_scale">1 Einheit = %1$s</string> + <string name="sno_shard_dataspace">Versteckt im Dataspace · %1$s</string> + <string name="sno_shard_ideaspace">Versteckt im Ideaspace · %1$s</string> + <string name="sno_avatar_default">Standard-Avatar</string> + <string name="sno_avatar_default_details">Keine Form veröffentlicht, daher sehen alle diesen hier</string> + <string name="cyberspace_bag_title">An einem Ort versteckt</string> + <string name="cyberspace_bag_box">Versteckt in einem Bereich von %1$s Regionen</string> + <string name="cyberspace_bag_destination">Versteckt in einer Region, die der Hinweis genau benennt</string> + <string name="cyberspace_bag_no_hint">Kein Hinweis, das könnte also überall im Cyberspace sein</string> + <string name="cyberspace_bag_out_of_reach">Außer Reichweite: Der Hinweis nennt 2^%1$d zu durchsuchende Regionen</string> + <string name="cyberspace_bag_search">Danach suchen</string> + <string name="cyberspace_bag_cancel">Stopp</string> + <string name="cyberspace_bag_measuring">Suchaufwand auf diesem Gerät wird ermittelt…</string> + <string name="cyberspace_bag_progress">%1$s von %2$s Regionen durchsucht</string> + <plurals name="cyberspace_bag_too_slow_minutes"> + <item quantity="one">Außer Reichweite: etwa %1$d Minute Suche auf diesem Gerät</item> + <item quantity="other">Außer Reichweite: etwa %1$d Minuten Suche auf diesem Gerät</item> + </plurals> + <plurals name="cyberspace_bag_too_slow_hours"> + <item quantity="one">Außer Reichweite: etwa %1$d Stunde Suche auf diesem Gerät</item> + <item quantity="other">Außer Reichweite: etwa %1$d Stunden Suche auf diesem Gerät</item> + </plurals> + <string name="cyberspace_bag_not_found">Nicht in dem Bereich, den der Hinweis nennt</string> + <string name="cyberspace_bag_damaged">Region gefunden, aber der Inhalt konnte nicht gelesen werden</string> + <plurals name="cyberspace_bag_dropped"> + <item quantity="one">%1$d hier verstecktes Objekt passte nicht zu seiner Signatur und wurde verworfen</item> + <item quantity="other">%1$d hier versteckte Objekte passten nicht zu ihren Signaturen und wurden verworfen</item> + </plurals> + <string name="cyberspace_bag_empty">Geöffnet, aber es war nichts drin</string> + <string name="cyberspace_bag_unsigned">Unsigniert, dieser Autor ist also nur eine Behauptung</string> + <string name="cyberspace_bag_unknown_kind">Ein Objekt vom Typ %1$d, den dieser Client nicht darstellt</string> + <plurals name="cyberspace_bag_opaque"> + <item quantity="one">%1$d Byte an Daten, die dieser Client nicht lesen kann</item> + <item quantity="other">%1$d Bytes an Daten, die dieser Client nicht lesen kann</item> + </plurals> + <string name="browser_pill_close">Schließen</string> + <string name="browser_pill_tile_copy">Link kopieren</string> + <string name="browser_pill_tile_find">Suchen</string> + <string name="browser_pill_tile_text">Textgröße</string> + <string name="browser_pill_tile_desktop">Desktop</string> + <string name="browser_pill_tile_home">Zum Startbildschirm</string> + <string name="browser_pill_tile_other">Anderer Browser</string> + <string name="browser_pill_tile_full">Vollbild</string> + <string name="browser_pill_edit_address">Adresse bearbeiten</string> + <string name="browser_pill_back">Zurück</string> + <string name="browser_pill_forward">Vorwärts</string> + <string name="browser_pill_reload">Neu laden</string> + <string name="browser_pill_stop">Laden stoppen</string> + <string name="browser_pill_favorite_add">Zu Favoriten hinzufügen</string> + <string name="browser_pill_favorite_remove">Aus Favoriten entfernen</string> + <string name="browser_pill_share">Teilen</string> + <string name="browser_pill_copy">Link kopieren</string> + <string name="browser_pill_find">Auf Seite suchen</string> + <string name="browser_pill_text_size">Textgröße</string> + <string name="browser_pill_add_home">Zum Startbildschirm hinzufügen</string> + <string name="browser_pill_desktop">Desktop-Version</string> + <string name="browser_pill_other_browser">Im Browser öffnen</string> + <string name="browser_pill_other_browser_named">In %1$s öffnen</string> + <string name="browser_pill_full_screen">Vollbild</string> + <string name="browser_pill_left_site">Du hast %1$s verlassen</string> + <string name="browser_pill_back_to_app">Zurück zur App</string> + <string name="browser_pill_text_smaller">Kleinerer Text</string> + <string name="browser_pill_text_larger">Größerer Text</string> + <string name="browser_pill_text_reset">Zurücksetzen</string> + <string name="browser_pill_text_value">%1$d%</string> + <string name="browser_pill_privacy">Privatsphäre</string> + <string name="browser_pill_tor_title">Onion-Routing</string> + <string name="browser_pill_tor_on">Die Website kann deine IP-Adresse nicht sehen</string> + <string name="browser_pill_tor_off">Die Website kann deine IP-Adresse sehen</string> + <string name="browser_pill_site_settings">Website-Einstellungen</string> + <string name="browser_pill_site_settings_none">Noch nichts erlaubt</string> + <string name="browser_pill_access">Worauf sie zugreifen kann</string> + <string name="browser_pill_access_desc">Deine Schlüssel verlassen Amethyst nie</string> + <string name="browser_pill_access_keys_desc">Jedes Signieren, Veröffentlichen, Hochladen oder Bezahlen braucht deine Zustimmung</string> + <string name="browser_pill_access_none">Kein besonderer Zugriff</string> + <string name="browser_pill_access_none_desc">Sie läuft in einer Sandbox und kann nicht in deinem Konto handeln</string> + <string name="browser_pill_access_manage">Berechtigungen verwalten</string> + <string name="browser_pill_access_nsite">nSite · Sandbox</string> + <string name="browser_pill_access_napplet">nApplet · Sandbox</string> + <string name="browser_pill_console">Konsole</string> + <string name="browser_pill_security_https">Sichere Verbindung</string> + <string name="browser_pill_security_http">Nicht sicher</string> + <string name="browser_pill_security_tor">Über Onion-Routing</string> + <string name="browser_pill_security_sandbox">App in Sandbox</string> + <string name="browser_pill_permission_camera">Kamera</string> + <string name="browser_pill_permission_microphone">Mikrofon</string> + <string name="browser_pill_permission_location">Standort</string> + <string name="browser_pill_decision_ask">Fragen</string> + <string name="browser_pill_decision_allow">Erlauben</string> + <string name="browser_pill_decision_block">Blockieren</string> + <string name="browser_pill_permission_state">%1$s %2$s</string> + <string name="browser_pill_decision_allowed">erlaubt</string> + <string name="browser_pill_decision_blocked">blockiert</string> + <string name="browser_pill_address_hint">Suchen oder Adresse eingeben</string> + <string name="browser_pill_go">Öffnen</string> + <string name="browser_pill_clear">Löschen</string> + <string name="browser_pill_paste_go">Einfügen und öffnen</string> + <string name="browser_pill_fill_in">Diese Adresse verwenden</string> + <string name="browser_pill_find_hint">Auf Seite suchen</string> + <string name="browser_pill_find_count">%1$d / %2$d</string> + <string name="browser_pill_find_none">Keine Treffer</string> + <string name="browser_pill_find_previous">Vorheriger Treffer</string> + <string name="browser_pill_find_next">Nächster Treffer</string> + <string name="browser_pill_find_close">Suche auf Seite schließen</string> + <string name="browser_pill_console_all">Alle</string> + <string name="browser_pill_console_errors">Fehler</string> + <string name="browser_pill_console_warnings">Warnungen</string> + <string name="browser_pill_console_copy">Kopieren</string> + <string name="browser_pill_console_clear">Löschen</string> + <string name="browser_pill_console_empty">Noch nichts protokolliert</string> + <string name="browser_pill_perm_title">Diese Website möchte Folgendes verwenden</string> + <string name="browser_pill_perm_camera_desc">Sehen, was deine Kamera sieht</string> + <string name="browser_pill_perm_microphone_desc">Hören, was dein Mikrofon hört</string> + <string name="browser_pill_perm_location_desc">Ungefähr wissen, wo du bist</string> + <string name="browser_pill_perm_tor_note">Anrufe können deine IP-Adresse auch über Tor preisgeben.</string> + <string name="browser_pill_perm_allow">Während des Besuchs erlauben</string> + <string name="browser_pill_perm_once">Nur dieses Mal</string> + <string name="browser_pill_perm_deny">Nicht erlauben</string> + <string name="browser_pill_dialog_says">%1$s meldet</string> + <string name="browser_pill_dialog_generic">Diese Seite meldet</string> + <string name="browser_pill_dialog_block">Diese Seite keine weiteren Dialoge anzeigen lassen</string> + <string name="browser_pill_dialog_answer">Deine Antwort</string> + <string name="browser_pill_dialog_leave_title">Website verlassen?</string> + <string name="browser_pill_dialog_leave_message">Vorgenommene Änderungen werden möglicherweise nicht gespeichert.</string> + <string name="browser_pill_dialog_leave">Verlassen</string> + <string name="browser_pill_ok">OK</string> + <string name="browser_pill_cancel">Abbrechen</string> + <string name="browser_pill_info_connection">Verbindung</string> + <string name="browser_pill_info_https">Verbindung ist verschlüsselt</string> + <string name="browser_pill_info_https_desc">Was du sendest, kann unterwegs nicht mitgelesen werden.</string> + <string name="browser_pill_info_http">Verbindung ist nicht verschlüsselt</string> + <string name="browser_pill_info_http_desc">Gib keine Passwörter oder Zahlungsdaten ein.</string> + <string name="browser_pill_info_tor">Über Tor per Onion-Routing</string> + <string name="browser_pill_info_open">Offenes Web</string> + <string name="browser_pill_info_certificate">Zertifikat</string> + <string name="browser_pill_info_certificate_desc">Ausgestellt für %1$s von %2$s · gültig bis %3$s</string> + <string name="browser_pill_info_permissions">Berechtigungen</string> + <string name="browser_pill_info_site_data">Cookies und Websitedaten</string> + <string name="browser_pill_info_site_data_desc">Anmeldungen und Einstellungen, die diese Website für dieses Konto gespeichert hat</string> + <string name="browser_pill_info_clear">Websitedaten löschen</string> + <string name="browser_pill_info_clear_confirm">Von dieser Website abmelden und ihre Daten löschen?</string> + <string name="backup_action_block_again">%1$s erneut blockieren</string> + <string name="backup_action_keep_count">%1$s behalten</string> + <string name="backup_action_rejoin">%1$s erneut beitreten</string> + <string name="backup_action_remute">%1$s erneut stummschalten</string> + <string name="backup_action_restore_count">%1$s wiederherstellen</string> + <string name="backup_action_restore_feeds">Feeds wiederherstellen</string> + <string name="backup_action_restore_places">Orte wiederherstellen</string> + <string name="backup_action_restore_profile">Profil wiederherstellen</string> + <string name="backup_action_restore_providers">Anbieter wiederherstellen</string> + <string name="backup_action_restore_relays">Relays wiederherstellen</string> + <string name="backup_action_restore_topics">Themen wiederherstellen</string> + <string name="backup_action_restore_wallet">Wallet wiederherstellen</string> + <string name="backup_card_accept">Übernehmen</string> + <plurals name="backup_card_and_added"> + <item quantity="one">%1$s und %2$d hinzugefügt</item> + <item quantity="other">%1$s und %2$d hinzugefügt</item> + </plurals> + <plurals name="backup_card_and_blocked"> + <item quantity="one">%1$s und %2$d neu blockiert</item> + <item quantity="other">%1$s und %2$d neu blockiert</item> + </plurals> + <plurals name="backup_card_and_followed"> + <item quantity="one">%1$s und folgst %2$d neuen Person</item> + <item quantity="other">%1$s und folgst %2$d neuen Personen</item> + </plurals> + <plurals name="backup_card_and_joined"> + <item quantity="one">%1$s und bist %2$d neuen beigetreten</item> + <item quantity="other">%1$s und bist %2$d neuen beigetreten</item> + </plurals> + <plurals name="backup_card_and_muted"> + <item quantity="one">%1$s und %2$d neu stummgeschaltet</item> + <item quantity="other">%1$s und %2$d neu stummgeschaltet</item> + </plurals> + <string name="backup_card_app_settings">Deine synchronisierten Amethyst-Einstellungen wurden in einer anderen App als Amethyst geändert. Das kann Probleme verursachen.</string> + <string name="backup_card_concord_emptied">Du hast deine Concord-Liste geleert</string> + <string name="backup_card_concord_rewritten">Du hast deine Concord-Liste geändert</string> + <plurals name="backup_card_dm_relays_lost"> + <item quantity="one">Du hast %1$d DM-Relay entfernt</item> + <item quantity="other">Du hast %1$d DM-Relays entfernt</item> + </plurals> + <plurals name="backup_card_feeds_removed"> + <item quantity="one">Du hast %1$d Lieblings-Feed entfernt</item> + <item quantity="other">Du hast %1$d Lieblings-Feeds entfernt</item> + </plurals> + <plurals name="backup_card_follows_dropped"> + <item quantity="one">Du folgst %1$d Person nicht mehr</item> + <item quantity="other">Du folgst %1$d Personen nicht mehr</item> + </plurals> + <plurals name="backup_card_hashtags_unfollowed"> + <item quantity="one">Du hast %1$d gefolgten Hashtag entfernt</item> + <item quantity="other">Du hast %1$d gefolgte Hashtags entfernt</item> + </plurals> + <plurals name="backup_card_indexer_relays_lost"> + <item quantity="one">Du hast %1$d Indexer-Relay entfernt</item> + <item quantity="other">Du hast %1$d Indexer-Relays entfernt</item> + </plurals> + <plurals name="backup_card_key_package_relays_lost"> + <item quantity="one">Du hast %1$d Relay für Gruppenchat-Schlüssel entfernt</item> + <item quantity="other">Du hast %1$d Relays für Gruppenchat-Schlüssel entfernt</item> + </plurals> + <plurals name="backup_card_leave_chats"> + <item quantity="one">Du hast %1$d öffentlichen Chat verlassen</item> + <item quantity="other">Du hast %1$d öffentliche Chats verlassen</item> + </plurals> + <plurals name="backup_card_leave_communities"> + <item quantity="one">Du hast %1$d Community verlassen</item> + <item quantity="other">Du hast %1$d Communities verlassen</item> + </plurals> + <plurals name="backup_card_leave_groups"> + <item quantity="one">Du hast %1$d Gruppe verlassen</item> + <item quantity="other">Du hast %1$d Gruppen verlassen</item> + </plurals> + <plurals name="backup_card_leave_rooms"> + <item quantity="one">Du hast %1$d Chatraum verlassen</item> + <item quantity="other">Du hast %1$d Chaträume verlassen</item> + </plurals> + <plurals name="backup_card_mutes_visible"> + <item quantity="one">Du hast die Stummschaltung für %1$d Eintrag aufgehoben</item> + <item quantity="other">Du hast die Stummschaltung für %1$d Einträge aufgehoben</item> + </plurals> + <plurals name="backup_card_nip65_lost"> + <item quantity="one">Du hast %1$d öffentliches Relay entfernt</item> + <item quantity="other">Du hast %1$d öffentliche Relays entfernt</item> + </plurals> + <string name="backup_card_nutzap_key_removed">Du hast deinen Nutzap-Schlüssel entfernt</string> + <string name="backup_card_nutzap_key_replaced">Du hast deinen Nutzap-Schlüssel ersetzt</string> + <plurals name="backup_card_nutzap_mints_removed"> + <item quantity="one">Du hast %1$d Nutzap-Mint entfernt</item> + <item quantity="other">Du hast %1$d Nutzap-Mints entfernt</item> + </plurals> + <plurals name="backup_card_offers_removed"> + <item quantity="one">Du hast %1$d BOLT12-Angebot entfernt</item> + <item quantity="other">Du hast %1$d BOLT12-Angebote entfernt</item> + </plurals> + <plurals name="backup_card_payment_targets_removed"> + <item quantity="one">Du hast %1$d Zahlungsweg entfernt</item> + <item quantity="other">Du hast %1$d Zahlungswege entfernt</item> + </plurals> + <plurals name="backup_card_places_unfollowed"> + <item quantity="one">Du hast %1$d gefolgten Ort entfernt</item> + <item quantity="other">Du hast %1$d gefolgte Orte entfernt</item> + </plurals> + <plurals name="backup_card_private_outbox_lost"> + <item quantity="one">Du hast %1$d privates Relay entfernt</item> + <item quantity="other">Du hast %1$d private Relays entfernt</item> + </plurals> + <plurals name="backup_card_profile_lost_fields"> + <item quantity="one">Du hast %1$d Profilfeld entfernt</item> + <item quantity="other">Du hast %1$d Profilfelder entfernt</item> + </plurals> + <plurals name="backup_card_relay_feeds_lost"> + <item quantity="one">Du hast %1$d Relay-Feed entfernt</item> + <item quantity="other">Du hast %1$d Relay-Feeds entfernt</item> + </plurals> + <plurals name="backup_card_search_relays_lost"> + <item quantity="one">Du hast %1$d Such-Relay entfernt</item> + <item quantity="other">Du hast %1$d Such-Relays entfernt</item> + </plurals> + <plurals name="backup_card_trust_services_changed"> + <item quantity="one">Du hast den Anbieter von %1$d Vertrauensdienst geändert</item> + <item quantity="other">Du hast den Anbieter von %1$d Vertrauensdiensten geändert</item> + </plurals> + <plurals name="backup_card_trusted_lost"> + <item quantity="one">Du hast %1$d vertrauenswürdiges Relay entfernt</item> + <item quantity="other">Du hast %1$d vertrauenswürdige Relays entfernt</item> + </plurals> + <plurals name="backup_card_unblocked"> + <item quantity="one">Du hast %1$d Relay entsperrt</item> + <item quantity="other">Du hast %1$d Relays entsperrt</item> + </plurals> + <string name="backup_card_wallet_emptied">Du hast deine Cashu-Wallet geleert</string> + <string name="backup_card_wallet_rewritten">Du hast deine Cashu-Wallet geändert</string> + <string name="backup_conflict_added">Hinzugefügt (%1$s)</string> + <string name="backup_conflict_changed">Geändert (%1$s)</string> + <string name="backup_conflict_count_added">%1$s hinzugefügt</string> + <string name="backup_conflict_count_changed">%1$s geändert</string> + <string name="backup_conflict_count_removed">%1$s entfernt</string> + <string name="backup_conflict_keep_new">Neue Version verwenden</string> + <string name="backup_conflict_private_added">Du hast private (verschlüsselte) Einträge hinzugefügt.</string> + <string name="backup_conflict_private_changed">Du hast auch private (verschlüsselte) Einträge geändert. Sie können hier nicht verglichen werden.</string> + <string name="backup_conflict_private_cleared">Du hast alle privaten (verschlüsselten) Einträge entfernt.</string> + <string name="backup_conflict_relay_read_only">nur lesen</string> + <string name="backup_conflict_relay_read_write">lesen und schreiben</string> + <string name="backup_conflict_relay_write_only">nur schreiben</string> + <string name="backup_conflict_removed">Entfernt (%1$s)</string> + <string name="backup_conflict_restore_mine">Gespeicherte Version wiederherstellen</string> + <string name="backup_conflict_title">%1$s in einer anderen App geändert</string> + <string name="backup_entry_algo_feed">Feeds</string> + <string name="backup_entry_chat_room">Chaträume</string> + <string name="backup_entry_community">Communities</string> + <string name="backup_entry_hashtag">Hashtags</string> + <string name="backup_entry_identity_claim">Verknüpfte Identitäten</string> + <string name="backup_entry_location">Orte</string> + <string name="backup_entry_mint">Mints</string> + <string name="backup_entry_nutzap_key">Nutzap-Schlüssel</string> + <string name="backup_entry_offer">Angebote</string> + <string name="backup_entry_other">Andere Einträge</string> + <string name="backup_entry_payment_target">Zahlungsadressen</string> + <string name="backup_entry_person">Personen</string> + <string name="backup_entry_profile_field">Profilfelder</string> + <string name="backup_entry_public_chat">Öffentliche Chats</string> + <string name="backup_entry_relay">Relays</string> + <string name="backup_entry_relay_group">Gruppen</string> + <string name="backup_entry_thread">Threads</string> + <string name="backup_entry_trust_provider">Vertrauensanbieter</string> + <string name="backup_entry_word">Wörter</string> + <string name="backup_profile_field_about">Über mich</string> + <string name="backup_profile_field_banner">Banner</string> + <string name="backup_profile_field_birthday">Geburtstag</string> + <string name="backup_profile_field_bot">Bot-Kennzeichnung</string> + <string name="backup_profile_field_clink_offer">CLINK-Angebot</string> + <string name="backup_profile_field_display_name">Anzeigename</string> + <string name="backup_profile_field_lud06">LNURL</string> + <string name="backup_profile_field_lud16">Lightning-Adresse</string> + <string name="backup_profile_field_name">Benutzername</string> + <string name="backup_profile_field_nip05">Nostr-Adresse</string> + <string name="backup_profile_field_picture">Bild</string> + <string name="backup_profile_field_pronouns">Pronomen</string> + <string name="backup_profile_field_website">Website</string> + <string name="backup_review_allowed_tag">Erlaubt</string> + <string name="backup_review_changed_by_other_app">In einer anderen App geändert · %1$s</string> + <string name="backup_review_concord_body">Deine Concord-Communities sind für dich verschlüsselt und können daher hier nicht verglichen werden. Beim Wiederherstellen wird genau das zurückgesetzt, was dieses Gerät gespeichert hat.</string> + <string name="backup_review_demoted_tag">Verschoben</string> + <string name="backup_review_dropped_count">%1$s entfernt</string> + <string name="backup_review_dropped_tag">Entfernt</string> + <string name="backup_review_encrypted_added">Von dir erstellt</string> + <string name="backup_review_encrypted_changed">Von dir geändert</string> + <string name="backup_review_encrypted_cleared">Von dir geleert</string> + <string name="backup_review_feed">Feed</string> + <string name="backup_review_gone">Weg</string> + <string name="backup_review_hashtags_you_follow">Hashtags, denen du folgst</string> + <string name="backup_review_inbox">Inbox</string> + <string name="backup_review_inbox_explainer">wo andere dich erreichen</string> + <string name="backup_review_joined">beigetreten</string> + <string name="backup_review_kept">behalten</string> + <string name="backup_review_kept_count">%1$s behalten</string> + <string name="backup_review_key_removed">Empfangsschlüssel entfernt</string> + <string name="backup_review_key_replaced">Empfangsschlüssel ersetzt</string> + <string name="backup_review_key_warning">Nutzaps werden an diesen neuen Schlüssel gebunden. Wenn dieses Gerät ihn nicht hat, kannst du sie hier möglicherweise nicht einlösen.</string> + <string name="backup_review_left">verlassen</string> + <string name="backup_review_left_tag">Verlassen</string> + <string name="backup_review_listed_tag">Gelistet</string> + <string name="backup_review_moved_tag">Verschoben</string> + <string name="backup_review_names_and_more">%1$s und %2$s weitere</string> + <string name="backup_review_new">Neue Version</string> + <string name="backup_review_new_count">%1$s neu</string> + <string name="backup_review_new_count_caption">neu</string> + <string name="backup_review_new_tag">Neu</string> + <string name="backup_review_newly_blocked">Neu blockiert</string> + <string name="backup_review_newly_muted">%1$s neu stummgeschaltet</string> + <string name="backup_review_no_longer_blocked">Nicht mehr blockiert</string> + <string name="backup_review_not_set">Nicht festgelegt</string> + <string name="backup_review_nothing_here">Hier ist nichts</string> + <string name="backup_review_offer">BOLT12-Angebot</string> + <plurals name="backup_review_offers_gone"> + <item quantity="one">%1$d Angebot entfernt</item> + <item quantity="other">%1$d Angebote entfernt</item> + </plurals> + <string name="backup_review_outbox">Outbox</string> + <string name="backup_review_outbox_explainer">wo du veröffentlichst</string> + <plurals name="backup_review_payment_targets_gone"> + <item quantity="one">%1$d Weg, dich zu bezahlen, entfernt</item> + <item quantity="other">%1$d Wege, dich zu bezahlen, entfernt</item> + </plurals> + <string name="backup_review_payments_explainer">Wer dich bezahlt, sieht nur, was in dieser Liste steht.</string> + <string name="backup_review_places_gone">Orte entfernt</string> + <string name="backup_review_places_you_follow">Orte, denen du folgst</string> + <string name="backup_review_private_added_body">Sie sind jetzt verschlüsselt — vermutlich sind die öffentlichen Einträge oben dorthin gewandert.</string> + <string name="backup_review_private_added_title">Du hast Einträge privat gemacht</string> + <string name="backup_review_private_changed_body">Sie sind verschlüsselt und können daher hier nicht verglichen werden.</string> + <string name="backup_review_private_changed_title">Du hast private Einträge geändert</string> + <string name="backup_review_private_wiped_body">Sie sind verschlüsselt, daher kannst nur du sehen, was sie waren. Beim Wiederherstellen kommen alle zurück.</string> + <string name="backup_review_private_wiped_title">Du hast alle privaten Einträge entfernt</string> + <string name="backup_review_receiving_tag">Empfang</string> + <string name="backup_review_renamed">umbenannt</string> + <string name="backup_review_renamed_tag">Umbenannt</string> + <string name="backup_review_saved">Auf diesem Gerät</string> + <plurals name="backup_review_search_people"> + <item quantity="one">%1$s Person durchsuchen</item> + <item quantity="other">%1$s Personen durchsuchen</item> + </plurals> + <string name="backup_review_searching_tag">Suche</string> + <string name="backup_review_service">Dienst</string> + <string name="backup_review_service_followers">Follower</string> + <string name="backup_review_service_rank">Rang</string> + <string name="backup_review_service_topics">Themen</string> + <plurals name="backup_review_services_changed"> + <item quantity="one">%1$s von %2$s Dienst geändert</item> + <item quantity="other">%1$s von %2$s Diensten geändert</item> + </plurals> + <string name="backup_review_services_changed_explainer">Sie kommen jetzt von jemand anderem oder von niemandem</string> + <string name="backup_review_still_blocked">Weiterhin blockiert</string> + <string name="backup_review_storing_tag">Speicherung</string> + <string name="backup_review_tab_dropped">Entfernt · %1$s</string> + <string name="backup_review_tab_edited">Bearbeitet · %1$s</string> + <string name="backup_review_tab_new">Neu · %1$s</string> + <string name="backup_review_title_cleared">%1$s geleert</string> + <string name="backup_review_title_updated">%1$s aktualisiert</string> + <string name="backup_review_topics_gone">Themen entfernt</string> + <string name="backup_review_trust_warning">Ränge, Follower-Zahlen und Spamfilter, die du siehst, stammen von diesen Anbietern.</string> + <string name="backup_review_trusted_tag">Vertrauenswürdig</string> + <string name="backup_review_unblocked">%1$s entsperrt</string> + <string name="backup_review_unblocked_explainer">Amethyst verbindet sich wieder mit ihnen</string> + <string name="backup_review_units_tag">Einheiten</string> + <string name="backup_review_unmuted">%1$s nicht mehr stummgeschaltet</string> + <string name="backup_review_unmuted_explainer">Diese können wieder in deinen Feeds erscheinen</string> + <string name="backup_review_untrusted_tag">Nicht vertrauenswürdig</string> + <string name="backup_review_wallet_body">Die Mints und Schlüssel deiner Wallet sind für dich verschlüsselt und können daher hier nicht einzeln verglichen werden. Beim Wiederherstellen wird genau das zurückgesetzt, was dieses Gerät gespeichert hat.</string> + <string name="backup_review_was_named">hieß „%1$s“</string> + <string name="backup_review_words_and_hashtags">Wörter & Hashtags</string> + <string name="backup_type_app_settings">Synchronisierte App-Einstellungen</string> + <string name="backup_type_app_settings_explainer">Amethyst-Einstellungen, die zwischen deinen Geräten synchronisiert werden. Sie sind verschlüsselt, daher kann nur ihr Vorhandensein verglichen werden.</string> + <string name="backup_type_blocked_relays">Liste blockierter Relays</string> + <string name="backup_type_blocked_relays_explainer">Relays, mit denen sich diese App nie verbindet.</string> + <string name="backup_type_bolt12_offers">BOLT12-Angebote</string> + <string name="backup_type_bolt12_offers_explainer">Die Lightning-Angebote, mit denen andere dich bezahlen.</string> + <string name="backup_type_cashu_wallet">Cashu-Wallet</string> + <string name="backup_type_cashu_wallet_explainer">Deine Ecash-Wallet-Einstellungen und Mints. Sie sind verschlüsselt, daher kann nur ihr Vorhandensein verglichen werden.</string> + <string name="backup_type_communities">Community-Liste</string> + <string name="backup_type_communities_explainer">Die Communities, denen du beigetreten bist.</string> + <string name="backup_type_concord_communities">Concord-Community-Liste</string> + <string name="backup_type_concord_communities_explainer">Die Concord-Communities, denen du beigetreten bist. Sie sind verschlüsselt, daher kann nur ihr Vorhandensein verglichen werden.</string> + <string name="backup_type_dm_relays">DM-Relay-Liste</string> + <string name="backup_type_dm_relays_explainer">Wohin andere deine privaten Nachrichten zustellen.</string> + <string name="backup_type_ephemeral_chats">Liste flüchtiger Chats</string> + <string name="backup_type_ephemeral_chats_explainer">Die flüchtigen Chaträume, denen du beigetreten bist.</string> + <string name="backup_type_favorite_algo_feeds">Liste der Lieblings-Feeds</string> + <string name="backup_type_favorite_algo_feeds_explainer">Die algorithmischen Feeds, die du gespeichert hast.</string> + <string name="backup_type_follow_list">Folgeliste</string> + <string name="backup_type_follow_list_explainer">Bestimmt, wessen Beiträge in deinem Home-Feed erscheinen.</string> + <string name="backup_type_geohashes">Gefolgte Orte</string> + <string name="backup_type_geohashes_explainer">Orte, deren Beiträge in deinen Feeds erscheinen.</string> + <string name="backup_type_hashtags">Gefolgte Hashtags</string> + <string name="backup_type_hashtags_explainer">Hashtags, deren Beiträge in deinen Feeds erscheinen.</string> + <string name="backup_type_indexer_relays">Indexer-Relay-Liste</string> + <string name="backup_type_indexer_relays_explainer">Die Relays, über die Profile und Relay-Listen anderer Personen gefunden werden.</string> + <string name="backup_type_key_package_relays">Relays für Gruppenchat-Schlüssel</string> + <string name="backup_type_key_package_relays_explainer">Wo andere die Schlüssel finden, um dich zu verschlüsselten Gruppenchats hinzuzufügen.</string> + <string name="backup_type_mute_list">Stummschaltliste</string> + <string name="backup_type_mute_list_explainer">Blendet Personen, Wörter, Hashtags und Threads überall aus, wo du etwas siehst.</string> + <string name="backup_type_nutzap_info">Nutzap-Empfangseinstellungen</string> + <string name="backup_type_nutzap_info_explainer">Die Mints und der Schlüssel, mit denen andere dir Nutzaps senden.</string> + <string name="backup_type_other">Liste</string> + <string name="backup_type_other_explainer">Eine auf diesem Gerät gespeicherte Liste.</string> + <string name="backup_type_outbox_inbox_relays">Öffentliche Relay-Liste</string> + <string name="backup_type_outbox_inbox_relays_explainer">Teilt allen mit, wo deine Beiträge zu finden sind und wohin Antworten und Erwähnungen an dich gesendet werden sollen.</string> + <string name="backup_type_payment_targets">Zahlungsadressen</string> + <string name="backup_type_payment_targets_explainer">Die Wege, auf denen andere dich bezahlen können.</string> + <string name="backup_type_private_outbox_relays">Private Relay-Liste</string> + <string name="backup_type_private_outbox_relays_explainer">Wo deine Entwürfe und privaten Daten gespeichert werden.</string> + <string name="backup_type_profile">Profil</string> + <string name="backup_type_profile_explainer">Dein Name, Bild, Bio und deine Zahlungsadressen, so wie alle sie sehen.</string> + <string name="backup_type_public_chats">Liste öffentlicher Chats</string> + <string name="backup_type_public_chats_explainer">Die öffentlichen Chats, denen du beigetreten bist.</string> + <string name="backup_type_relay_feeds">Lieblings-Relay-Feeds</string> + <string name="backup_type_relay_feeds_explainer">Die Relays, die du als Feeds durchstöberst.</string> + <string name="backup_type_relay_groups">Gruppenliste</string> + <string name="backup_type_relay_groups_explainer">Die Relay-basierten Gruppen, denen du beigetreten bist.</string> + <string name="backup_type_search_relays">Such-Relay-Liste</string> + <string name="backup_type_search_relays_explainer">Die Relays, die bei deiner Suche verwendet werden.</string> + <string name="backup_type_trust_providers">Liste der Vertrauensanbieter</string> + <string name="backup_type_trust_providers_explainer">Die Dienste, denen du vertraust, Personen für dich zu bewerten und zu verifizieren.</string> + <string name="backup_type_trusted_relays">Liste vertrauenswürdiger Relays</string> + <string name="backup_type_trusted_relays_explainer">Relays, denen du vertraust, sodass ohne Nachfrage eine Verbindung aufgebaut wird.</string> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-pt-rBR/strings.xml b/commonsUI/src/commonMain/composeResources/values-pt-rBR/strings.xml index fdc35e9309..33a24e3cbd 100644 --- a/commonsUI/src/commonMain/composeResources/values-pt-rBR/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-pt-rBR/strings.xml @@ -4922,4 +4922,1174 @@ one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> + <string name="geocache_unnamed">Geocache</string> + <string name="geocache_type_traditional">Tradicional</string> + <string name="geocache_type_multi">Multi-cache</string> + <string name="geocache_type_mystery">Mistério</string> + <string name="geocache_size_micro">Micro</string> + <string name="geocache_size_small">Pequeno</string> + <string name="geocache_size_regular">Regular</string> + <string name="geocache_size_large">Grande</string> + <string name="geocache_size_other">Outro tamanho</string> + <string name="geocache_difficulty_short">D%1$d</string> + <string name="geocache_terrain_short">T%1$d</string> + <string name="geocache_first_to_find">Primeiro a encontrar</string> + <string name="geocache_is_art">Arte</string> + <string name="geocache_mission">Key Quest</string> + <string name="geocache_needs_verification">Encontros verificados</string> + <string name="geocache_claimed">Reivindicado</string> + <string name="geocache_archived">Arquivado</string> + <string name="geocache_hint_tap_to_reveal">Travou? Toque para revelar a dica</string> + <string name="geocache_hint_other_reading">Ilegível? Toque para ver a outra leitura</string> + <string name="geocache_nearby">Por perto</string> + <string name="geocache_found_it">Encontrei!</string> + <string name="geocache_verified_find">Encontro verificado</string> + <string name="geocache_invalid_proof">A prova não confere</string> + <string name="geocache_loading">Carregando cache…</string> + <string name="geocache_hidden_by">Escondido por %1$s</string> + <string name="geocache_navigate">Navegar</string> + <string name="geocache_copy_coordinates">Copiar coordenadas</string> + <string name="geocache_coordinates_copied">Coordenadas copiadas</string> + <string name="geocache_photos_section">Fotos</string> + <string name="geocache_mission_section">Missão Key Quest</string> + <string name="geocache_logs_section">Logs</string> + <string name="geocache_no_logs">Ninguém registrou este cache ainda. Seja o primeiro.</string> + <string name="geocache_i_found_it">Encontrei</string> + <string name="geocache_didnt_find_it">Não encontrei</string> + <string name="geocache_add_note">Adicionar uma nota</string> + <string name="geocache_needs_maintenance">Informar que precisa de manutenção</string> + <string name="geocache_add_to_hunt">Adicionar a uma caçada</string> + <string name="geocache_hunt_new">Nova caçada…</string> + <string name="geocache_hunt_none_yet">Você ainda não criou uma caçada.</string> + <string name="geocache_hunt_already_on">Já está nesta caçada</string> + <plurals name="geocache_caches_here"> + <item quantity="one">%1$d cache aqui</item> + <item quantity="other">%1$d caches aqui</item> + </plurals> + <string name="geocache_photo_remove">Remover esta foto</string> + <string name="geocache_archived_notice">Este cache foi arquivado. Ele não está mais em jogo.</string> + <string name="geocache_ftf_won_by">Primeiro a encontrar: %1$s</string> + <string name="geocache_ftf_available">O primeiro a encontrar ainda está em aberto</string> + <string name="geocache_owner_actions">Dono</string> + <string name="geocache_owner_edit">Editar este cache</string> + <string name="geocache_owner_archive">Arquivar este cache</string> + <string name="geocache_owner_archive_confirm">Arquivar. Quem procurar verá que ele está fora de jogo, e a listagem continua legível.</string> + <string name="geocache_owner_lock_ftf">Confirmar o primeiro a encontrar</string> + <string name="geocache_owner_lock_ftf_explain">Publica uma tag F indicando %1$s. Isso é permanente.</string> + <string name="geocache_owner_no_finders">Ainda não há encontros verificados, então não há ninguém para confirmar.</string> + <string name="geocache_owner_show_qr">Mostrar o código de verificação</string> + <string name="geocache_owner_qr_warning">Imprima isto e coloque dentro do cache. O Amethyst não o armazena — se você perdê-lo, quem encontrar o cache não poderá mais provar que esteve lá.</string> + <string name="geocache_log_type_dnf">Não encontrei</string> + <string name="geocache_log_type_note">Nota</string> + <string name="geocache_log_type_maintenance">Precisa de manutenção</string> + <string name="geocache_log_title">Registrar um encontro</string> + <string name="geocache_log_cache_label">Cache</string> + <string name="geocache_log_your_log">Seu log</string> + <string name="geocache_log_placeholder">Como foi?</string> + <string name="geocache_log_add_photo">Adicionar uma foto</string> + <string name="geocache_log_mission_answer">Sua resposta da Key Quest</string> + <string name="geocache_log_proof_section">Prova de presença</string> + <string name="geocache_log_scan_code">Escaneie o código dentro do cache</string> + <string name="geocache_log_scan_again">Escanear outro código</string> + <string name="geocache_log_proof_verified">Verificado — você esteve no cache</string> + <string name="geocache_log_proof_explain">Assinado uma única vez com o código da caixa. A chave nunca é salva e nunca sai desta tela.</string> + <string name="geocache_log_proof_bad_code">Isso não é um código de verificação.</string> + <string name="geocache_log_proof_wrong_cache">Esse código pertence a outro cache.</string> + <string name="geocache_log_proof_optional">Este cache não exige prova, então você pode simplesmente registrá-lo.</string> + <string name="geocache_log_no_location_attached">Sua própria localização não é anexada a este log.</string> + <string name="geocache_log_post">Publicar</string> + <string name="geocache_log_comment_title">Adicionar um log</string> + <string name="geocache_tab_nearby">Por perto</string> + <string name="geocache_tab_map">Mapa</string> + <string name="geocache_tab_hunts">Caçadas</string> + <string name="geocache_tab_finds">Encontrados</string> + <string name="geocache_tab_mine">Meus</string> + <string name="geocache_filter_unfound">Não encontrados</string> + <string name="geocache_filter_difficulty">D ≤ %1$d</string> + <string name="geocache_filter_terrain">T ≤ %1$d</string> + <string name="geocache_filter_has_prize">Tem prêmio</string> + <string name="geocache_filter_verified_only">Exige prova</string> + <string name="geocache_use_my_location">Usar minha localização</string> + <string name="geocache_sorted_by_distance">Ordenado por distância</string> + <string name="geocache_sorted_by_recent">Mais recentes primeiro</string> + <string name="geocache_hide_a_cache">Esconder um cache</string> + <string name="geocache_empty_nearby">Ainda não há caches aqui. Tente uma lista de seguidos mais ampla ou esconda o primeiro.</string> + <string name="geocache_empty_finds">Você ainda não registrou nenhum encontro.</string> + <string name="geocache_empty_mine">Você ainda não escondeu nenhum cache.</string> + <string name="geocache_empty_hunts">Nenhuma caçada ainda.</string> + <string name="geocache_stat_finds">Encontros</string> + <string name="geocache_stat_ftf">Primeiros a encontrar</string> + <string name="geocache_stat_verified">Verificados</string> + <string name="geocache_stat_hidden">Escondidos</string> + <string name="geocache_map_recenter">Centralizar em mim</string> + <string name="geocache_map_long_press">Toque e segure no mapa para esconder um cache ali</string> + <string name="geocache_map_open">Abrir</string> + <string name="geocache_new_section_where">Onde</string> + <string name="geocache_new_section_what">O quê</string> + <string name="geocache_new_section_extras">Extras</string> + <string name="geocache_new_section_proof">Prova</string> + <string name="geocache_new_section_relays">Relays</string> + <string name="geocache_new_location_warning">Isto publica uma localização pública com precisão de cerca de 5 metros. Qualquer pessoa no Nostr pode vê-la, e um evento publicado não pode ser retirado.</string> + <string name="geocache_new_pick_location">Escolha o local</string> + <string name="geocache_new_ladder">Publica %1$s, além de células mais amplas de até 3 caracteres para a busca por proximidade.</string> + <string name="geocache_new_name">Nome</string> + <string name="geocache_new_description">Descrição</string> + <string name="geocache_new_type">Tipo</string> + <string name="geocache_new_size">Tamanho</string> + <string name="geocache_new_difficulty">Dificuldade</string> + <string name="geocache_new_terrain">Terreno</string> + <string name="geocache_new_hint">Dica</string> + <string name="geocache_new_hint_preview">Quem procurar verá</string> + <string name="geocache_new_hint_scramble">Embaralhar a dica (ROT13)</string> + <string name="geocache_new_mission">Missão Key Quest</string> + <string name="geocache_new_modifiers">Modificadores</string> + <string name="geocache_new_require_proof">Exigir prova de presença</string> + <string name="geocache_new_proof_ready">Uma chave de verificação foi gerada para este cache.</string> + <string name="geocache_new_log_relays">Onde os encontros devem ser registrados</string> + <string name="geocache_new_publish">Publicar</string> + <string name="geocache_new_needs_name">Dê um nome ao cache.</string> + <string name="geocache_new_needs_location">Escolha onde o cache está escondido.</string> + <string name="geocache_hunt_progress">%1$d de %2$d encontrados</string> + <string name="geocache_hunt_start">Começar a caçada</string> + <plurals name="geocache_hunt_caches"> + <item quantity="one">%1$d cache</item> + <item quantity="other">%1$d caches</item> + </plurals> + <string name="geocache_hunt_title">Título</string> + <string name="geocache_hunt_description">Descrição</string> + <string name="geocache_hunt_banner">Imagem de capa</string> + <string name="geocache_hunt_pick_caches">Caches, em ordem</string> + <string name="geocache_hunt_add_cache">Adicionar um cache</string> + <string name="geocache_hunt_move_up">Mover para cima</string> + <string name="geocache_hunt_move_down">Mover para baixo</string> + <string name="geocache_hunt_remove">Remover</string> + <string name="geocache_hunt_theme">Tema da página</string> + <string name="geocache_hunt_map_style">Estilo do mapa</string> + <string name="geocache_hunt_needs_title">Dê um título à caçada.</string> + <string name="geocache_hunt_needs_caches">Adicione pelo menos um cache.</string> + <string name="geocache_publish_failed">Não foi possível publicar. Verifique sua conexão com os relays e tente novamente.</string> + <string name="cordn_groups_title">Grupos Cordn</string> + <string name="cordn_badge_coordinator">Grupo com coordenador</string> + <string name="cordn_badge_coordinator_desc">Entregue por um coordenador, não por relays</string> + <string name="cordn_exposure_title">O que este coordenador pode ver</string> + <string name="cordn_exposure_subtitle">Um coordenador é um único servidor que transporta todas as mensagens deste grupo, em ordem. É uma troca diferente da dos relays — vale saber disso antes de tratar isto como qualquer outro chat.</string> + <string name="cordn_exposure_dimension_content">Conteúdo das mensagens</string> + <string name="cordn_exposure_dimension_membership">Quem está no grupo</string> + <string name="cordn_exposure_dimension_messaging">Quem envia o quê</string> + <string name="cordn_exposure_level_none">Ilegível</string> + <string name="cordn_exposure_level_pseudonymous">Sob uma chave descartável</string> + <string name="cordn_exposure_level_identified">Vinculado à sua conta real</string> + <string name="cordn_exposure_details">Detalhes</string> + <string name="cordn_exposure_coordinator">Coordenador %1$s</string> + <string name="cordn_exposure_differs">Esta não é a mesma exposição de um grupo Marmot, em que nenhum operador sozinho detém a conversa inteira.</string> + <string name="cordn_note_membership">Entrar identifica contas reais nas duas pontas, então o coordenador sabe quem está neste grupo.</string> + <string name="cordn_note_linked">Uma única chave descartável envia e busca para todos os seus grupos aqui, então o coordenador consegue perceber que todos são seus.</string> + <string name="cordn_note_history">Um único operador detém o histórico completo e ordenado de todos os grupos que transporta.</string> + <string name="cordn_note_publication">O pacote de chaves que você publicou é um registro assinado de que esta conta usa cordn, e o coordenador pode entregá-lo a qualquer pessoa.</string> + <string name="cordn_note_sizes">As mensagens não recebem preenchimento, então o coordenador vê o tamanho de cada uma.</string> + <string name="cordn_note_encryption_bug">Este cliente não está criptografando para o coordenador. Isso é um bug — por favor, reporte.</string> + <string name="cordn_health_unknown">Ainda não contatado</string> + <string name="cordn_health_ok">Respondendo</string> + <string name="cordn_health_down">Sem resposta</string> + <plurals name="cordn_health_failures"> + <item quantity="one">%1$d tentativa seguida com falha</item> + <item quantity="other">%1$d tentativas seguidas com falha</item> + </plurals> + <string name="cordn_link_explainer">Cole um link cordn1… que alguém compartilhou com você para ver qual coordenador transporta esse grupo e o que esse operador saberia sobre você se você entrasse.</string> + <string name="cordn_link_field">cordn1…</string> + <string name="cordn_link_inspect">Inspecionar</string> + <string name="cordn_link_paste">Colar</string> + <string name="cordn_link_clear">Limpar</string> + <string name="cordn_link_invalid">Isso não é um link de grupo cordn: %1$s</string> + <string name="cordn_link_group_id">ID do grupo</string> + <string name="cordn_link_coordinator">Coordenador</string> + <string name="cordn_link_relays">Acessível por</string> + <string name="cordn_link_no_coordinator">Este link não indica nenhum coordenador, então não pode ser seguido sozinho. Pergunte a quem o enviou qual coordenador transporta o grupo.</string> + <string name="cordn_link_not_joinable">Inspecionar um link não entra em nada, e ter um link não torna você membro.</string> + <string name="cordn_link_title">Link de grupo Cordn</string> + <string name="cordn_link_search_keywords">cordn coordenador grupo link convite mls chat metadados privacidade exposição</string> + <string name="cordn_coordinators_title">Coordenadores Cordn</string> + <string name="cordn_coordinators_search_keywords">cordn coordenador adicionar remover expurgar saúde servidor mls chat ordenação</string> + <string name="cordn_keypackages_title">Pacotes de chaves Cordn</string> + <string name="cordn_keypackages_search_keywords">cordn pacote de chaves publicar convite último recurso coordenador mls</string> + <string name="cordn_backup_title">Backup do Cordn</string> + <string name="cordn_backup_search_keywords">cordn backup restaurar exportar importar senha grupos recuperação</string> + <string name="cordn_group_untitled">Grupo %1$s</string> + <string name="cordn_group_no_messages_yet">Nenhuma mensagem ainda</string> + <string name="cordn_group_via_coordinator">Grupo Cordn, transportado por %1$s</string> + <string name="cordn_preview_deleted">Mensagem excluída</string> + <string name="cordn_preview_voice_note">Mensagem de voz</string> + <string name="cordn_preview_photo">Foto</string> + <string name="cordn_preview_video">Vídeo</string> + <string name="cordn_preview_file">Arquivo</string> + <string name="cordn_invitations_entry">Convidado para um grupo cordn?</string> + <string name="cordn_invitations_entry_action">Ver convites</string> + <string name="cordn_hub_title">Cordn</string> + <string name="cordn_hub_explainer">Grupos entregues por um coordenador que você escolhe, em vez de por relays.</string> + <string name="cordn_hub_link">Inspecionar um link de grupo</string> + <string name="cordn_hub_link_desc">Veja para onde um link cordn1… aponta antes de agir</string> + <string name="cordn_migrate_section_send">Sair deste celular</string> + <string name="cordn_migrate_section_receive">Assumir a partir de outro celular</string> + <string name="cordn_keypackages_section">Pacotes de chaves publicados</string> + <string name="cordn_link_section_input">O link</string> + <string name="cordn_coordinators_section_yours">Coordenadores que você usa</string> + <string name="cordn_coordinators_section_discover">Anunciando-se</string> + <string name="cordn_coordinators_section_manual">Adicionar manualmente</string> + <string name="cordn_backup_section_passphrase">Senha</string> + <string name="cordn_backup_section_export">Fazer um backup</string> + <string name="cordn_backup_section_restore">Restaurar de um backup</string> + <string name="cordn_hub_section_service">O coordenador</string> + <string name="cordn_hub_section_device">Este dispositivo</string> + <string name="cordn_hub_coordinators">Coordenadores</string> + <string name="cordn_hub_coordinators_desc">Os servidores que transportam seus grupos</string> + <string name="cordn_hub_keypackages">Pacotes de chaves</string> + <string name="cordn_hub_keypackages_desc">O que permite que as pessoas adicionem você a um grupo</string> + <string name="cordn_hub_backup">Backup e restauração</string> + <string name="cordn_hub_backup_desc">Um arquivo criptografado com senha que fica com você</string> + <string name="cordn_hub_migrate">Mudar para um novo celular</string> + <string name="cordn_hub_migrate_desc">Passe seus grupos para o dispositivo que vai substituir este</string> + <string name="cordn_migrate_title">Mudar para um novo celular</string> + <string name="cordn_migrate_explainer">Seus grupos cordn ficam neste dispositivo. Movê-los exige um código: este celular os publica, o novo celular os lê e este celular para.</string> + <string name="cordn_migrate_sign_in_first">Primeiro entre com a mesma conta no novo celular. Isto move seus grupos, nunca sua chave.</string> + <string name="cordn_migrate_export">Iniciar a transferência</string> + <string name="cordn_migrate_exporting">Publicando…</string> + <string name="cordn_migrate_scan_this">Escaneie isto no novo celular</string> + <string name="cordn_migrate_code_note">O código é apenas um ponteiro. Quem fotografá-lo não descobre nada — aquilo para que ele aponta está criptografado para você.</string> + <string name="cordn_migrate_handed_off">Este dispositivo transferiu seus grupos</string> + <string name="cordn_migrate_handed_off_desc">Ele parou de enviar e receber, porque dois celulares enviando no mesmo grupo o quebrariam para todos nele. Seus grupos continuam neste dispositivo e nada foi excluído.</string> + <string name="cordn_migrate_cancel">Retomar este dispositivo</string> + <string name="cordn_migrate_cancel_desc">Use isto se a mudança não terminou. Só faça isso se o novo celular ainda não começou a enviar.</string> + <string name="cordn_migrate_receive">Receber de outro celular</string> + <string name="cordn_migrate_scan">Escanear o código</string> + <string name="cordn_migrate_paste">ou cole-o</string> + <string name="cordn_migrate_import">Trazer os grupos para cá</string> + <string name="cordn_migrate_importing">Buscando…</string> + <string name="cordn_migrate_replaces_warning">Isto substitui todos os grupos cordn que já estão neste celular. Eles não podem ser mesclados.</string> + <plurals name="cordn_migrate_done"> + <item quantity="one">%1$d grupo movido</item> + <item quantity="other">%1$d grupos movidos</item> + </plurals> + <string name="cordn_migrate_no_groups">Não há grupos cordn neste dispositivo para mover.</string> + <string name="cordn_migrate_no_server">Defina primeiro um servidor de mídia — os documentos criptografados precisam de um lugar para ficar enquanto o outro celular os busca.</string> + <string name="cordn_migrate_exposure_title">O que sai deste dispositivo</string> + <string name="cordn_migrate_exposure_body">O estado dos seus grupos é criptografado e enviado ao seu servidor de mídia para que o outro celular possa buscá-lo. Ele é ilegível para esse servidor, que vê apenas um tamanho e um horário. Exclua os blobs depois se não quiser deixar nada para trás.</string> + <string name="cordn_action_delete">Excluir</string> + <string name="cordn_action_edit">Editar</string> + <string name="cordn_action_editing">Editando sua mensagem</string> + <string name="cordn_action_go_to_message">Ir para a mensagem</string> + <string name="cordn_action_pin">Fixar</string> + <string name="cordn_action_reply">Responder</string> + <string name="cordn_action_unpin">Desafixar</string> + <string name="cordn_admin_action_failed">Não foi possível fazer essa alteração.</string> + <string name="cordn_admin_coordinator_silent">O coordenador não respondeu, então nada foi alterado. Tente novamente em instantes.</string> + <string name="cordn_admin_no_key_package">%1$s não publicou um pacote de chaves neste coordenador, então ainda não pode ser adicionado. Peça que abra o cordn neste coordenador primeiro.</string> + <string name="cordn_admin_no_self_remove">Você não pode remover a si mesmo de um grupo cordn.</string> + <string name="cordn_admin_no_welcome">Não foi possível dar a %1$s um meio de entrar no grupo, então a adição foi cancelada.</string> + <string name="cordn_admin_not_a_member">%1$s não está neste grupo.</string> + <string name="cordn_admin_someone">Essa pessoa</string> + <string name="cordn_admin_wrong_key_package">Este coordenador forneceu um pacote de chaves que pertence a outra pessoa, então %1$s não foi adicionado. Nada foi alterado.</string> + <string name="cordn_backup_contents_body">As chaves de criptografia dos seus grupos, seus pacotes de chaves e todas as mensagens desses grupos. Qualquer pessoa que abrir o arquivo pode ler as conversas. A senha é a única coisa que o protege depois que ele sai do Amethyst.</string> + <string name="cordn_backup_contents_title">O que há no arquivo</string> + <string name="cordn_backup_explainer">Sua nsec não recupera grupos cordn. Um grupo cordn existe como estado de criptografia neste dispositivo e como um fluxo ordenado em um coordenador que não consegue lê-lo — perca o dispositivo sem um backup e o grupo se vai, incluindo tudo o que já foi dito nele.</string> + <string name="cordn_backup_export">Exportar</string> + <string name="cordn_backup_exported">Backup salvo.</string> + <string name="cordn_backup_failed">Não funcionou. Verifique a senha e o arquivo.</string> + <string name="cordn_backup_passphrase">Senha</string> + <string name="cordn_backup_restore">Restaurar</string> + <string name="cordn_backup_restore_body">Restaurar substitui os grupos cordn deste dispositivo pelos do arquivo. Serve para um celular que substituiu outro — não é uma forma de usar os mesmos grupos em dois dispositivos ao mesmo tempo.</string> + <string name="cordn_backup_restore_confirm_body">Todos os grupos cordn atualmente neste dispositivo serão removidos e substituídos pelos do arquivo.\n\nNão restaure em um segundo celular enquanto o primeiro ainda estiver em uso. Ambos teriam as mesmas chaves de grupo e, assim que os dois enviarem uma mensagem, o grupo quebra para todos — silenciosamente e sem volta.</string> + <string name="cordn_backup_restore_confirm_title">Substituir os grupos cordn deste dispositivo?</string> + <string name="cordn_backup_restore_title">Restaurar</string> + <string name="cordn_backup_restored">Restaurado.</string> + <string name="cordn_chat_empty_description">Tudo o que for enviado aqui é criptografado para este grupo. O coordenador repassa as mensagens sem conseguir lê-las.</string> + <string name="cordn_chat_empty_title">Nenhuma mensagem ainda</string> + <string name="cordn_chat_unread_divider">Novas mensagens</string> + <string name="cordn_chat_yesterday">Ontem</string> + <string name="cordn_composer_hint">Mensagem</string> + <string name="cordn_coordinators_add_action">Adicionar</string> + <string name="cordn_coordinators_add_failed">Não foi possível abrir esse coordenador.</string> + <string name="cordn_coordinators_copy_key">Copiar a npub do coordenador</string> + <string name="cordn_coordinators_discover_action">Procurar coordenadores</string> + <string name="cordn_coordinators_discover_add">Adicionar</string> + <string name="cordn_coordinators_discover_explainer">Lê anúncios que seus relays já transportam. Nenhum coordenador é contatado, então nenhum deles fica sabendo que você procurou.</string> + <string name="cordn_coordinators_discover_failed">Não foi possível ler os anúncios</string> + <string name="cordn_coordinators_discover_none">Ninguém está se anunciando nos seus relays.</string> + <string name="cordn_coordinators_discover_seen">Anunciado há %1$s</string> + <string name="cordn_coordinators_discover_seen_on">Último anúncio: %1$s</string> + <plurals name="cordn_coordinators_discover_unheard"> + <item quantity="one">Nada encontrado, e %1$d dos seus relays não respondeu.</item> + <item quantity="other">Nada encontrado, e %1$d dos seus relays não responderam.</item> + </plurals> + <string name="cordn_coordinators_explainer">Um coordenador ordena as mensagens de um grupo e é o único que pode fazer isso. Perdê-lo significa perder a ordenação, e um segundo coordenador não espelha o primeiro — por isso são escolhas nomeadas por você, não relays intercambiáveis.</string> + <plurals name="cordn_coordinators_health_down"> + <item quantity="one">Sem resposta — %1$d chamada seguida falhou. Os grupos nele não estão sincronizando.</item> + <item quantity="other">Sem resposta — %1$d chamadas seguidas falharam. Os grupos nele não estão sincronizando.</item> + </plurals> + <string name="cordn_coordinators_health_ok">Respondendo.</string> + <string name="cordn_coordinators_health_retrying">Uma chamada falhou; tentando novamente.</string> + <string name="cordn_coordinators_health_unknown">Nada foi solicitado a ele ainda.</string> + <string name="cordn_coordinators_hide_older">Ocultar os que pararam de se anunciar</string> + <string name="cordn_coordinators_identify">Perguntar quem é</string> + <string name="cordn_coordinators_key">Chave do coordenador</string> + <string name="cordn_coordinators_label">Um nome para ele (seu, opcional)</string> + <string name="cordn_coordinators_label_note">Este nome é seu e fica neste dispositivo. Um coordenador não pode comprovar um nome, então nunca é solicitado um.</string> + <string name="cordn_coordinators_more">Mais ações</string> + <string name="cordn_coordinators_none">Nenhum coordenador ainda.</string> + <string name="cordn_coordinators_purge">Expurgar</string> + <string name="cordn_coordinators_purge_body">Remover mantém os grupos neste dispositivo, então adicionar o coordenador de volta os traz junto. Expurgar os exclui: as chaves dos grupos, as posições de leitura e seus pacotes de chaves são apagados, e esses grupos só podem ser reabertos com um novo convite.\n\nO coordenador não é avisado e mantém tudo o que já tinha.</string> + <string name="cordn_coordinators_purge_title">Expurgar este coordenador?</string> + <string name="cordn_coordinators_relays">%1$s</string> + <string name="cordn_coordinators_relays_label">Responde em</string> + <string name="cordn_coordinators_relays_more">%1$s +%2$d mais</string> + <string name="cordn_coordinators_remove">Remover</string> + <string name="cordn_coordinators_rename">Renomear</string> + <string name="cordn_coordinators_server">Diz ser: %1$s</string> + <string name="cordn_coordinators_server_claim">O nome e a versão de um coordenador são textos escolhidos por ele. A chave pública é a única coisa que o identifica.</string> + <string name="cordn_coordinators_server_silent">Respondeu, mas não informou nenhum nome.</string> + <string name="cordn_coordinators_show_all">Mostrar todos os %1$d</string> + <string name="cordn_coordinators_show_fewer">Mostrar menos</string> + <plurals name="cordn_coordinators_show_older"> + <item quantity="one">Mostrar %1$d que parou de se anunciar</item> + <item quantity="other">Mostrar %1$d que pararam de se anunciar</item> + </plurals> + <string name="cordn_coordinators_stale_note">Estes não se anunciam há mais de um mês. Criar um grupo em um que tenha sumido vai falhar.</string> + <string name="cordn_create_action">Criar grupo</string> + <plurals name="cordn_create_action_invite"> + <item quantity="one">Criar e convidar %1$d pessoa</item> + <item quantity="other">Criar e convidar %1$d pessoas</item> + </plurals> + <string name="cordn_create_add_member">Adicionar alguém</string> + <string name="cordn_create_admin">Admin</string> + <plurals name="cordn_create_admin_count"> + <item quantity="one">%1$d pessoa pode adicionar e remover</item> + <item quantity="other">%1$d pessoas podem adicionar e remover</item> + </plurals> + <string name="cordn_create_admin_only_me">Só eu gerencio este grupo</string> + <string name="cordn_create_admin_only_me_on">Só você pode convidar, remover membros ou alterar os detalhes do grupo. Você pode passar isso para outra pessoa depois.</string> + <string name="cordn_create_coordinator">Coordenador</string> + <string name="cordn_create_coordinator_change">Alterar</string> + <string name="cordn_create_coordinator_discover">Encontrar coordenadores</string> + <string name="cordn_create_coordinator_hide">Concluído</string> + <string name="cordn_create_coordinator_new">Outro coordenador</string> + <string name="cordn_create_coordinator_pubkey">Chave pública do coordenador (hex ou npub)</string> + <string name="cordn_create_coordinator_relays">Relays em que ele responde, um por linha</string> + <plurals name="cordn_create_coverage_all"> + <item quantity="one">Alcança a %1$d pessoa escolhida</item> + <item quantity="other">Alcança todas as %1$d pessoas escolhidas</item> + </plurals> + <string name="cordn_create_coverage_partial">Alcança %1$d de %2$d — as demais ficariam de fora</string> + <string name="cordn_create_coverage_unknown">Não verificado — escolhê-lo fará a consulta</string> + <string name="cordn_create_description">Descrição (opcional)</string> + <string name="cordn_create_egalitarian">Todos podem adicionar e remover</string> + <string name="cordn_create_egalitarian_note">Todos no grupo podem convidar, remover membros e alterar os detalhes. Qualquer membro pode nomear admins depois, o que encerra esse modo.</string> + <string name="cordn_create_explainer">Um grupo cordn é ordenado por um coordenador que você escolhe. A criação do grupo acontece neste dispositivo — o coordenador só fica sabendo que o grupo existe quando você convida alguém ou envia uma mensagem.</string> + <string name="cordn_create_failed">Não foi possível criar o grupo.</string> + <string name="cordn_create_name">Nome do grupo</string> + <string name="cordn_create_no_coordinator">Nenhum coordenador escolhido</string> + <string name="cordn_create_outcome_explainer">Cada convite é um Welcome deixado no coordenador, então eles podem falhar um a um. Ninguém foi notificado: as pessoas verão o convite na próxima vez que abrirem o app.</string> + <string name="cordn_create_outcome_failed">O coordenador recusou o convite</string> + <string name="cordn_create_outcome_link">Enviar um link</string> + <string name="cordn_create_outcome_no_welcome">Adicionado ao grupo, mas o convite não chegou — tente novamente pela tela do grupo</string> + <string name="cordn_create_outcome_open">Abrir o chat</string> + <string name="cordn_create_outcome_skipped">Sem chave aqui, então nenhum Welcome pôde ser deixado</string> + <string name="cordn_create_outcome_title">O grupo foi criado</string> + <string name="cordn_create_outcome_waiting">Aguardando a pessoa</string> + <string name="cordn_create_people_done">Concluído</string> + <string name="cordn_create_people_empty">Ninguém adicionado ainda. Um grupo pode ser criado vazio e os convites enviados depois, mas indicar as pessoas agora é o que mostra qual coordenador consegue alcançá-las.</string> + <string name="cordn_create_people_none">Ninguém ainda — toque para adicionar pessoas</string> + <string name="cordn_create_people_note">Indicar as pessoas agora é o que nos diz qual coordenador consegue alcançá-las. Cada uma recebe um Welcome deixado nesse coordenador, que ela recebe na próxima vez que abrir o app.</string> + <plurals name="cordn_create_reach_count"> + <item quantity="one">Alcançável em %1$d coordenador</item> + <item quantity="other">Alcançável em %1$d coordenadores</item> + </plurals> + <string name="cordn_create_reach_none">Sem chave em nenhum coordenador que você usa</string> + <string name="cordn_create_reach_unknown">Ainda não verificado</string> + <string name="cordn_create_step_admins">Quem pode adicionar e remover</string> + <string name="cordn_create_step_name">Dê um nome</string> + <string name="cordn_create_step_people">Quem participa</string> + <string name="cordn_create_step_where">Onde ele fica</string> + <string name="cordn_create_title">Novo grupo Cordn</string> + <plurals name="cordn_create_unreachable_count"> + <item quantity="one">%1$d pessoa não pode ser alcançada por nenhum coordenador que você usa</item> + <item quantity="other">%1$d pessoas não podem ser alcançadas por nenhum coordenador que você usa</item> + </plurals> + <string name="cordn_create_you">Você, sempre admin</string> + <string name="cordn_delete_confirm_body">Todos no grupo verão esta mensagem como retirada, e o Amethyst deixará de mostrar o texto. O coordenador ainda guarda a cópia criptografada que foi enviada, e quem já a leu, já leu.</string> + <string name="cordn_delete_confirm_title">Retirar mensagem</string> + <string name="cordn_delivery_accepted">Aceita pelo coordenador</string> + <string name="cordn_exposure_close">Entendi</string> + <string name="cordn_exposure_open">O que este coordenador pode ver</string> + <string name="cordn_group_info">Informações do grupo</string> + <string name="cordn_group_unavailable">Este grupo não está disponível. O coordenador dele pode ter sido removido.</string> + <string name="cordn_group_unavailable_detail">Adicione um coordenador nas Configurações para começar a usar grupos cordn.</string> + <plurals name="cordn_groups_count"> + <item quantity="one">%1$d grupo</item> + <item quantity="other">%1$d grupos</item> + </plurals> + <string name="cordn_groups_down_note">Nada está chegando destes grupos. O Amethyst continua tentando.</string> + <string name="cordn_groups_manage">Coordenadores</string> + <string name="cordn_groups_none">Nenhum grupo cordn ainda</string> + <string name="cordn_groups_none_detail">Um grupo cordn fica em um coordenador que guarda a lista de membros e repassa as mensagens, sem nunca ver o que alguém escreve.</string> + <string name="cordn_groups_start">Criar um</string> + <string name="cordn_info_add_member">Adicionar alguém</string> + <string name="cordn_info_add_member_none">Ninguém encontrado com esse nome.</string> + <string name="cordn_info_add_member_note">Só é possível adicionar quem publicou um pacote de chaves neste coordenador.</string> + <string name="cordn_info_add_member_placeholder">Nome, npub ou nome@domínio</string> + <string name="cordn_info_admin_badge">Admin</string> + <plurals name="cordn_info_admins_kept"> + <item quantity="one">O %1$d admin do grupo continua como está.</item> + <item quantity="other">Os %1$d admins do grupo continuam como estão.</item> + </plurals> + <string name="cordn_info_coordinator">Coordenador</string> + <string name="cordn_info_coordinator_key">Chave do coordenador</string> + <string name="cordn_info_coordinator_nprofile">Link do coordenador</string> + <string name="cordn_info_copy_nprofile">Copiar o nprofile do coordenador</string> + <string name="cordn_info_edit_details">Editar detalhes</string> + <string name="cordn_info_egalitarian">Este grupo não tem admins: todos podem adicionar, remover e renomear, permanentemente.</string> + <string name="cordn_info_epoch">Época</string> + <string name="cordn_info_gid">ID do grupo</string> + <string name="cordn_info_has_key_package">Pode ser adicionado neste coordenador</string> + <string name="cordn_info_members">Membros</string> + <string name="cordn_info_remove_confirm_body">%1$s deixa de receber as mensagens daqui. A pessoa mantém o que já leu, e isso não pode ser desfeito — ela teria de ser convidada novamente.</string> + <string name="cordn_info_remove_confirm_title">Remover do grupo?</string> + <string name="cordn_info_remove_member">Remover</string> + <string name="cordn_info_save">Salvar</string> + <string name="cordn_info_technical">Detalhes técnicos</string> + <string name="cordn_invitations_accept">Entrar</string> + <string name="cordn_invitations_decline">Recusar</string> + <string name="cordn_invitations_decline_warning">Recusar é permanente. Para voltar, será preciso ser convidado novamente.</string> + <string name="cordn_invitations_explainer">Verificado quando você abre esta tela, e em nenhum outro momento — perguntar a um coordenador se alguém convidou você revela a ele que você está aqui, por isso o Amethyst não faz isso em segundo plano.</string> + <string name="cordn_invitations_load_failed">Não foi possível ler os convites.</string> + <plurals name="cordn_invitations_members"> + <item quantity="one">%1$d pessoa já está neste grupo</item> + <item quantity="other">%1$d pessoas já estão neste grupo</item> + </plurals> + <string name="cordn_invitations_members_more">+%1$d mais</string> + <string name="cordn_invitations_no_coordinators">Você não tem nenhum coordenador aberto. Os convites do cordn chegam por um coordenador, então ainda não há onde procurar.</string> + <string name="cordn_invitations_none">Nenhum convite pendente.</string> + <string name="cordn_invitations_refresh">Verificar novamente</string> + <string name="cordn_invitations_skipped">Um convite foi deixado para outro dispositivo</string> + <string name="cordn_invitations_title">Convites Cordn</string> + <string name="cordn_invitations_unreachable">%1$s não respondeu</string> + <string name="cordn_invitations_via">Por %1$s</string> + <string name="cordn_keypackages_disclosure">Publicar assina um registro com a chave da sua própria conta. Esse coordenador passa a saber que esta conta existe e pode ser convidada — permanentemente, e quer alguém convide você ou não.</string> + <string name="cordn_keypackages_explainer">Outras pessoas só podem adicionar você a um grupo cordn usando um pacote de chaves que você publicou nesse coordenador. O cordn não tem outro lugar para guardá-lo, então sem nenhum publicado ninguém pode convidar você — e nada diz a elas o porquê.</string> + <string name="cordn_keypackages_failed">Não foi possível ler os pacotes de chaves.</string> + <string name="cordn_keypackages_last_resort_no">Nenhum pacote de último recurso. Quando os de uso único acabarem, os convites falharão.</string> + <string name="cordn_keypackages_last_resort_yes">Um pacote de último recurso está publicado, então ainda é possível convidar você quando os de uso único acabarem.</string> + <plurals name="cordn_keypackages_orphans"> + <item quantity="one">%1$d pacote que este dispositivo não consegue abrir</item> + <item quantity="other">%1$d pacotes que este dispositivo não consegue abrir</item> + </plurals> + <string name="cordn_keypackages_orphans_body">O coordenador entregará estes pacotes a quem for convidar você, mas a metade privada não está neste dispositivo — ela pertence a outra instalação. Se essa instalação não existir mais, um convite que use um deles gera um Welcome que ninguém jamais conseguirá abrir.</string> + <string name="cordn_keypackages_publish">Publicar um</string> + <string name="cordn_keypackages_publish_last_resort">Publicar de último recurso</string> + <plurals name="cordn_keypackages_summary"> + <item quantity="one">%1$d pacote de uso único disponível.</item> + <item quantity="other">%1$d pacotes de uso único disponíveis.</item> + </plurals> + <string name="cordn_keypackages_topup_note">Depois que você publicar aqui, o Amethyst repõe discretamente os pacotes conforme forem usados. Ele nunca publica o primeiro por você.</string> + <string name="cordn_keypackages_withdraw_all">Retirar todos</string> + <string name="cordn_keypackages_withdraw_orphans">Retirar esses</string> + <string name="cordn_link_request">Pedir para entrar neste grupo</string> + <string name="cordn_link_request_disclosure">Isto publica um pacote de chaves com a chave da sua própria conta e informa ao coordenador que você quer entrar neste grupo — quer alguém responda ou não.</string> + <string name="cordn_link_request_failed">Não foi possível pedir para entrar.</string> + <string name="cordn_link_request_sent">Pedido enviado. Um membro do grupo precisa adicionar você; o convite aparecerá em Convites Cordn.</string> + <string name="cordn_link_scan">Escanear</string> + <string name="cordn_media_download_failed">Não foi possível abrir esse arquivo.</string> + <string name="cordn_media_no_server">Nenhum servidor de mídia está definido para esta conta, então não há onde colocar o arquivo. Escolha um nas Configurações.</string> + <string name="cordn_media_unreadable">Não foi possível ler esse arquivo.</string> + <string name="cordn_media_upload_failed">Não foi possível enviar esse arquivo.</string> + <plurals name="cordn_member_count"> + <item quantity="one">%1$d membro</item> + <item quantity="other">%1$d membros</item> + </plurals> + <string name="cordn_message_deleted">Mensagem excluída</string> + <string name="cordn_message_details_coordinator">Coordenador</string> + <string name="cordn_message_details_cursor">Cursor</string> + <string name="cordn_message_details_sent_at">Enviada</string> + <string name="cordn_message_details_title">Detalhes da mensagem</string> + <string name="cordn_message_edited">editada</string> + <string name="cordn_pinned_by">Fixada por %1$s</string> + <plurals name="cordn_pinned_count"> + <item quantity="one">%1$d mensagem fixada neste grupo</item> + <item quantity="other">%1$d mensagens fixadas neste grupo</item> + </plurals> + <string name="cordn_pinned_next">Próxima mensagem fixada</string> + <string name="cordn_pinned_previous">Mensagem fixada anterior</string> + <string name="cordn_pinned_show_all">Mostrar todas as mensagens fixadas</string> + <string name="cordn_pinned_title">Mensagens fixadas</string> + <string name="cordn_reactions_title">Reações</string> + <string name="cordn_requests_accept">Adicionar</string> + <string name="cordn_requests_admin_only">Só um admin pode aceitar. Em um grupo sem admins, isso vale para todos.</string> + <string name="cordn_requests_check">Verificar pedidos</string> + <string name="cordn_requests_decline">Dispensar</string> + <string name="cordn_requests_failed">Não foi possível ler os pedidos.</string> + <string name="cordn_requests_none">Ninguém está esperando para entrar.</string> + <string name="cordn_requests_title">Pedidos para entrar</string> + <string name="cordn_send">Enviar</string> + <string name="cordn_send_failed">Não foi enviada.</string> + <string name="cordn_send_no_session">O coordenador deste grupo não está aberto, então ainda não é possível enviar nada.</string> + <string name="cordn_share_copied">Copiado</string> + <string name="cordn_share_copy">Copiar link</string> + <string name="cordn_share_explainer">Qualquer pessoa com este link pode pedir para entrar. Um admin ainda precisa aceitá-la.</string> + <string name="cordn_share_title">Compartilhar este grupo</string> + <string name="cordn_voice_play">Reproduzir mensagem de voz</string> + <string name="cordn_voice_record">Gravar uma mensagem de voz</string> + <string name="cordn_voice_stop">Parar e enviar</string> + <string name="qr_scanner_camera_rationale">O Amethyst precisa da câmera para ler códigos QR. Nada é gravado nem enviado — os quadros são decodificados no dispositivo e descartados.</string> + <string name="qr_scanner_grant_camera">Permitir câmera</string> + <string name="qr_scanner_camera_blocked">O acesso à câmera está desativado para o Amethyst. Você pode reativá-lo nas configurações do Android.</string> + <string name="qr_scanner_open_settings">Abrir configurações</string> + <string name="qr_scanner_unavailable">O leitor de QR não pôde ser iniciado neste dispositivo. Você ainda pode colar o código como texto.</string> + <string name="qr_scanner_torch_on">Acender a lanterna</string> + <string name="qr_scanner_torch_off">Apagar a lanterna</string> + <string name="qr_scanner_dark_hint">Está escuro — experimente a lanterna</string> + <string name="qr_scanner_scan_image">Ler de uma imagem</string> + <string name="qr_scanner_paste">Colar</string> + <string name="qr_scanner_no_code_in_image">Nenhum código QR encontrado nessa imagem</string> + <string name="qr_scanner_clipboard_empty">Nada na área de transferência para ler</string> + <string name="qr_scanner_pick_one">Toque no código que você quer</string> + <plurals name="qr_scanner_sequence_progress"> + <item quantity="one">%1$s de %2$s parte capturada — continue</item> + <item quantity="other">%1$s de %2$s partes capturadas — continue</item> + </plurals> + <string name="qr_scanner_zoom_reset">Redefinir zoom</string> + <string name="qr_scanner_unsupported_title">O Amethyst não consegue abrir este código</string> + <string name="qr_scanner_unsupported_secret">Este código contém uma chave privada ou um segredo de pareamento, por isso o conteúdo não é exibido aqui. Use a tela que espera por ele.</string> + <string name="qr_scanner_kind_nsec">Isto é uma chave privada. Cole-a na tela de login.</string> + <string name="qr_scanner_kind_wallet">Isto é uma conexão de carteira. Adicione-a nas configurações da Carteira.</string> + <string name="qr_scanner_kind_bunker">Isto é um endereço de assinador remoto. O Amethyst fornece esses endereços para que outros apps possam pedir que ele assine — não é possível entrar com um deles.</string> + <string name="qr_scanner_kind_signer">Isto é um app pedindo para se conectar ao seu assinador. Abra-o pela tela do assinador NIP-46.</string> + <string name="qr_scanner_kind_lightning">Isto é uma fatura Lightning.</string> + <string name="qr_scanner_kind_cashu">Isto é um token Cashu.</string> + <string name="qr_scanner_kind_web">Isto é um link da web.</string> + <string name="qr_scanner_kind_text">Isto é texto simples, não um código Nostr.</string> + <string name="qr_scanner_kind_nostr_unsupported">Isto é um código Nostr, mas não um que esta tela consiga abrir.</string> + <string name="qr_scanner_label_profile">Perfil</string> + <string name="qr_scanner_label_note">Nota</string> + <string name="qr_scanner_label_nostr">Código Nostr</string> + <string name="qr_scanner_label_relay">Relay</string> + <string name="qr_scanner_label_nsec">Chave privada</string> + <string name="qr_scanner_label_wallet">Conexão de carteira</string> + <string name="qr_scanner_label_bunker">Endereço de assinador remoto</string> + <string name="qr_scanner_label_signer">Pedido de conexão ao assinador</string> + <string name="qr_scanner_label_lightning">Fatura Lightning</string> + <string name="qr_scanner_label_cashu">Token Cashu</string> + <string name="qr_scanner_label_web">Link da web</string> + <string name="qr_scanner_label_text">Texto simples</string> + <string name="qr_scanner_open_link">Abrir link</string> + <string name="qr_scanner_copy">Copiar</string> + <string name="qr_scanner_copied">Copiado</string> + <string name="qr_scanner_try_again">Ler novamente</string> + <string name="workout_suggestion_connect_title">Use os treinos que você já registrou</string> + <string name="workout_suggestion_connect_message">Conecte o Health Connect (Samsung Health, Google Fit, Fitbit, Garmin…) para montar a Minha Forma Física, seu próprio resumo de treinos, e para preencher este treino a partir de um que seu relógio já registrou, em vez de digitar cada número à mão.</string> + <string name="health_connect_rationale_headline">O Amethyst usa o Health Connect para montar a Minha Forma Física — seu próprio resumo de treinos, no seu celular.</string> + <string name="health_connect_rationale_intro">A Minha Forma Física (menu → Você → Minha Forma Física) mostra como você tem treinado: quanto fez nesta semana em comparação com a anterior, como seu tempo se divide entre corrida, ciclismo, caminhada, natação e academia, seus melhores desempenhos, quantos dias você treinou e sua sequência atual. O Amethyst monta esse panorama a partir dos treinos que seu relógio ou app de fitness já salvou no Health Connect, junto com os treinos que você registrou no próprio Amethyst. Tudo é calculado no seu celular e mostrado a você.</string> + <string name="health_connect_rationale_exercise">Exercício · qual atividade você fez, quando começou e quanto durou. É o treino em si: define sua contagem de treinos, seu tempo de treino, a divisão por atividade, seus dias ativos e sua sequência.</string> + <string name="health_connect_rationale_distance">Distância · até onde você foi, mostrada como sua distância nesta semana em comparação com a anterior, sua média semanal, sua distância por atividade e sua maior distância.</string> + <string name="health_connect_rationale_calories">Calorias ativas e totais · a energia gasta em um treino, mostrada como sua energia nesta semana e como ela se compara com a semana anterior. As calorias ativas são usadas quando sua fonte as registra; as calorias totais são a alternativa para fontes que só registram a energia total.</string> + <string name="health_connect_rationale_heart_rate">Frequência cardíaca · a média e o máximo de cada treino, combinados na sua frequência cardíaca média e mais alta do período. É o que diferencia uma semana leve de uma pesada com a mesma distância.</string> + <string name="health_connect_rationale_steps">Passos · a contagem de passos de uma corrida, caminhada ou trilha, mostrada como sua média semanal de passos e sua maior contagem de passos.</string> + <string name="health_connect_rationale_elevation">Ganho de elevação · quanto você subiu, mostrado como sua média semanal de subida e sua maior subida. É o que separa uma semana plana de uma com morros.</string> + <string name="health_connect_rationale_limit_window">Só lê treinos das últimas 4 semanas, e só enquanto a Minha Forma Física ou o editor de treinos estiver aberto. Nunca lê em segundo plano.</string> + <string name="health_connect_rationale_limit_publish">Nunca envia seus dados de saúde para lugar nenhum. A Minha Forma Física é calculada no seu celular e fica nele. Se depois você quiser publicar um treino, toque em Compartilhar nesse treino, leia a publicação que o Amethyst preencheu e publique você mesmo — um treino por vez, nunca automaticamente.</string> + <string name="health_connect_rationale_limit_optional">O recurso inteiro é opcional. Revogue as permissões no Health Connect a qualquer momento — a Minha Forma Física volta a exibir o convite para conectar — ou desative as sugestões do editor em Configurações → Configurações de composição. O resto do Amethyst continua funcionando.</string> + <string name="my_fitness_title">Minha Forma Física</string> + <string name="my_fitness_window">Últimas 4 semanas</string> + <string name="my_fitness_loading_metrics">Carregando distância, calorias e frequência cardíaca…</string> + <string name="my_fitness_window_note">As últimas quatro semanas completas do seu treino: os treinos que você publicou, mais tudo o que o Health Connect registra neste celular. O Amethyst calcula o resumo no dispositivo e não publica nada dele.</string> + <string name="my_fitness_this_week">Esta semana</string> + <string name="my_fitness_vs_last_week">vs. semana passada</string> + <string name="my_fitness_weekly_average">Média semanal</string> + <string name="my_fitness_workouts">Treinos</string> + <string name="my_fitness_active_days">Dias ativos</string> + <string name="my_fitness_streak">Dias seguidos</string> + <string name="my_fitness_time">Tempo</string> + <string name="my_fitness_duration_hours_minutes">%1$d h %2$d min</string> + <string name="my_fitness_duration_minutes">%1$d min</string> + <string name="my_fitness_duration_seconds">%1$d s</string> + <string name="my_fitness_distance">Distância</string> + <string name="my_fitness_calories">Calorias</string> + <string name="my_fitness_steps">Passos</string> + <string name="my_fitness_elevation">Subida</string> + <string name="my_fitness_avg_heart_rate">FC média</string> + <string name="my_fitness_max_heart_rate">FC máx.</string> + <string name="my_fitness_by_activity">Por atividade</string> + <string name="my_fitness_bests">Melhores desempenhos</string> + <string name="my_fitness_best_longest_distance">Maior distância</string> + <string name="my_fitness_best_longest_duration">Treino mais longo</string> + <string name="my_fitness_best_biggest_climb">Maior subida</string> + <string name="my_fitness_best_most_steps">Mais passos</string> + <string name="my_fitness_best_highest_heart_rate">Maior frequência cardíaca</string> + <string name="my_fitness_recent">Treinos recentes</string> + <string name="my_fitness_empty">Nada registrado nas últimas 4 semanas. Os treinos que você publicar, e os que seu relógio registrar, aparecem aqui.</string> + <string name="my_fitness_connect_title">Acompanhe seu treino</string> + <string name="my_fitness_connect_message">Conecte o Health Connect para ver seus totais semanais, a tendência semana a semana, seus melhores desempenhos e sua sequência de treinos. Tudo fica no seu celular.</string> + <string name="my_fitness_connect_banner">Conecte o Health Connect para adicionar os treinos que seu relógio registra, com frequência cardíaca, passos e subida.</string> + <string name="my_fitness_connect_button">Conectar</string> + <string name="my_fitness_share">Compartilhar este treino</string> + <string name="my_fitness_unit_bpm">bpm</string> + <string name="my_fitness_unit_kcal">kcal</string> + <string name="my_fitness_unit_km">km</string> + <string name="my_fitness_unit_mi">mi</string> + <string name="my_fitness_unit_m">m</string> + <string name="my_fitness_unit_ft">pés</string> + <string name="looking_for_event_title">Carregando</string> + <string name="looking_for_private_event">Aguardando suas mensagens privadas carregarem…</string> + <string name="call_duration_minutes_seconds">%1$02d:%2$02d</string> + <string name="call_duration_hours_minutes_seconds">%1$d:%2$02d:%3$02d</string> + <string name="marmot_preview_group_updated">Grupo atualizado</string> + <string name="marmot_preview_media">Anexo</string> + <string name="marmot_preview_no_text">Mensagem</string> + <string name="marmot_preview_with_sender">%1$s: %2$s</string> + <string name="marmot_empty_create_action">Criar um grupo</string> + <string name="marmot_invite_device_banner">Os convites do Marmot estão indo para outro dispositivo e não podem ser abertos aqui.</string> + <string name="marmot_invite_device_banner_dismiss">Dispensar</string> + <string name="marmot_invite_device_publish">Publicar a partir deste dispositivo</string> + <string name="wallet_open_zapped_note">Abrir nota que recebeu o zap</string> + <string name="video_player_settings_action_captions">Legendas</string> + <string name="video_player_settings_action_captions_description">Ativar ou desativar as legendas (oculto quando o vídeo não tem nenhuma)</string> + <string name="app_notification_reply_failed">Não enviada</string> + <string name="app_notification_reply_retry">Tentar novamente</string> + <string name="app_notification_reply_sending">Enviando…</string> + <string name="app_notification_reply_unconfirmed">Ainda enviando…</string> + <string name="app_notification_shortcut_account_removed">Esta conta foi removida do Amethyst.</string> + <string name="captions_off">Desativadas</string> + <string name="captions_turn_off">Desativar legendas</string> + <string name="captions_turn_on">Ativar legendas</string> + <string name="captions_unnamed_track">Legendas</string> + <string name="kind_app_curation_set">Lista de apps</string> + <string name="kind_article_curation_set">Lista de artigos</string> + <string name="kind_bookmark_set">Conjunto de favoritos</string> + <string name="kind_favorite_algo_feeds">Feeds favoritos</string> + <string name="kind_git_authors">Autores Git</string> + <string name="kind_git_repositories">Repositórios Git</string> + <string name="kind_good_wiki_authors">Autores de wiki confiáveis</string> + <string name="kind_good_wiki_relays">Relays de wiki confiáveis</string> + <string name="kind_interest_set">Interesses</string> + <string name="kind_media_follows">Seguidos de mídia</string> + <string name="kind_media_starter_pack">Pacote inicial de mídia</string> + <string name="kind_mute_set_for_kind">Silenciado para o kind %1$s</string> + <string name="kind_picture_curation_set">Lista de imagens</string> + <string name="kind_simple_groups">Grupos</string> + <string name="kind_video_collaboration">Colaboração em vídeo</string> + <string name="kind_video_list">Lista de vídeos</string> + <string name="kind_video_subtitles">Legendas de vídeo</string> + <string name="login_bunker_not_supported">O Amethyst para Android não consegue entrar com um endereço bunker://. Ele fornece esses endereços para que outros apps possam pedir que ele assine, mas não é possível entrar com um deles. Use um app assinador externo ou o Amethyst Desktop.</string> + <string name="login_nostrconnect_not_supported">Isso é um app pedindo para se conectar ao seu assinador, não uma forma de entrar. Entre primeiro e depois abra-o pela tela do assinador NIP-46.</string> + <string name="my_fitness">Minha Forma Física</string> + <string name="nip51_list_has_private_members">Esta lista tem membros privados que só o autor pode ver</string> + <string name="route_edit_geocache">Editar cache</string> + <string name="route_edit_geocache_hunt">Editar caçada</string> + <string name="route_geocache_detail">Cache</string> + <string name="route_geocache_hunt_detail">Caçada</string> + <string name="route_geocache_hunts">Caçadas de caches</string> + <string name="route_geocaches">Geocaches</string> + <string name="route_log_geocache_find">Registrar um achado</string> + <string name="route_new_geocache">Esconder um cache</string> + <string name="route_new_geocache_hunt">Nova caçada</string> + <string name="video_collaboration_accepted_on">Aceitou um crédito de colaboração</string> + <string name="video_collaboration_declined_on">Recusou um crédito de colaboração</string> + <string name="video_credit_accepted">Crédito aceito</string> + <string name="video_credit_audio_from">Áudio de</string> + <string name="video_credit_featuring">Participação de</string> + <string name="video_credit_inspired_by">Inspirado em</string> + <string name="video_credit_references">Referências</string> + <string name="video_curation_set_private_items">Lista privada</string> + <plurals name="video_curation_set_video_count"> + <item quantity="one">%1$d vídeo</item> + <item quantity="other">%1$d vídeos</item> + </plurals> + <string name="marmot_invite_device_success">Este dispositivo agora recebe seus convites do Marmot.</string> + <string name="marmot_invite_device_failure">Falha ao publicar o KeyPackage: %1$s</string> + <string name="marmot_invite_device_rejected">Nenhum relay aceitou o KeyPackage. Verifique suas configurações de relays e tente novamente.</string> + <string name="chat_delivery_sending">Enviando…</string> + <string name="chat_delivery_failed">Não foi possível enviar. Toque para tentar novamente.</string> + <plurals name="marmot_unread_messages"> + <item quantity="one">%1$d nova mensagem</item> + <item quantity="other">%1$d novas mensagens</item> + </plurals> + <string name="chat_type_cordn_title">Grupos Cordn</string> + <string name="chat_type_cordn_desc">Grupos com criptografia de ponta a ponta, ordenados por um coordenador que você escolhe, em vez de por relays.</string> + <string name="new_conversation_cordn_title">Grupo Cordn</string> + <string name="new_conversation_cordn_tagline">Chat em grupo criptografado, ordenado por um coordenador que você escolhe.</string> + <string name="new_conversation_cordn_chip">Coordenado</string> + <string name="new_conversation_cordn_best">Ideal para equipes que querem uma ordem confiável única para a conversa.</string> + <string name="new_conversation_cordn_cta">Criar grupo Cordn</string> + <string name="new_conversation_cordn_pro_1">Criptografia de ponta a ponta com MLS — o coordenador nunca consegue ler uma mensagem.</string> + <string name="new_conversation_cordn_pro_2">Uma única ordem acordada para todos, então o histórico não pode ser embaralhado por um relógio errado.</string> + <string name="new_conversation_cordn_con_1">O coordenador fica sabendo quem está no grupo e quando vocês conversam, mesmo sem conseguir ler o que é dito.</string> + <string name="sno_object_title">Objeto 3D</string> + <string name="sno_object_details">%1$d vértices · %2$d faces</string> + <string name="sno_object_unreadable">Não foi possível ler este objeto 3D (regra %1$s)</string> + <string name="sno_avatar_title">Avatar do Cyberspace</string> + <string name="sno_avatar_unpaid">Este avatar não pagou pelo seu tamanho, por isso não é desenhado</string> + <string name="sno_light_on">Iluminado; toque para desenhar o objeto plano</string> + <string name="sno_light_off">Plano; toque para iluminar o objeto</string> + <string name="sno_object_scale">1 unidade = %1$s</string> + <string name="sno_shard_dataspace">Escondido no dataspace · %1$s</string> + <string name="sno_shard_ideaspace">Escondido no ideaspace · %1$s</string> + <string name="sno_avatar_default">Avatar padrão</string> + <string name="sno_avatar_default_details">Nenhuma forma publicada, então todos veem este</string> + <string name="cyberspace_bag_title">Escondido em um lugar</string> + <string name="cyberspace_bag_box">Escondido em uma caixa de %1$s regiões</string> + <string name="cyberspace_bag_destination">Escondido em uma região, que a dica indica exatamente</string> + <string name="cyberspace_bag_no_hint">Sem dica, então pode estar em qualquer lugar do ciberespaço</string> + <string name="cyberspace_bag_out_of_reach">Fora de alcance: a dica indica 2^%1$d regiões para buscar</string> + <string name="cyberspace_bag_search">Procurar</string> + <string name="cyberspace_bag_cancel">Parar</string> + <string name="cyberspace_bag_measuring">Estimando o custo da busca neste dispositivo…</string> + <string name="cyberspace_bag_progress">%1$s de %2$s regiões verificadas</string> + <plurals name="cyberspace_bag_too_slow_minutes"> + <item quantity="one">Fora de alcance: cerca de %1$d minuto de busca neste dispositivo</item> + <item quantity="other">Fora de alcance: cerca de %1$d minutos de busca neste dispositivo</item> + </plurals> + <plurals name="cyberspace_bag_too_slow_hours"> + <item quantity="one">Fora de alcance: cerca de %1$d hora de busca neste dispositivo</item> + <item quantity="other">Fora de alcance: cerca de %1$d horas de busca neste dispositivo</item> + </plurals> + <string name="cyberspace_bag_not_found">Não está na caixa que a dica indicou</string> + <string name="cyberspace_bag_damaged">A região foi encontrada, mas o conteúdo não pôde ser lido</string> + <plurals name="cyberspace_bag_dropped"> + <item quantity="one">%1$d item escondido aqui não correspondia à sua assinatura e foi descartado</item> + <item quantity="other">%1$d itens escondidos aqui não correspondiam às suas assinaturas e foram descartados</item> + </plurals> + <string name="cyberspace_bag_empty">Aberto, e não havia nada dentro</string> + <string name="cyberspace_bag_unsigned">Sem assinatura, então este autor é apenas uma alegação</string> + <string name="cyberspace_bag_unknown_kind">Um item do kind %1$d, que este cliente não desenha</string> + <plurals name="cyberspace_bag_opaque"> + <item quantity="one">%1$d byte de algo que este cliente não lê</item> + <item quantity="other">%1$d bytes de algo que este cliente não lê</item> + </plurals> + <string name="browser_pill_close">Fechar</string> + <string name="browser_pill_tile_copy">Copiar link</string> + <string name="browser_pill_tile_find">Localizar</string> + <string name="browser_pill_tile_text">Tamanho do texto</string> + <string name="browser_pill_tile_desktop">Desktop</string> + <string name="browser_pill_tile_home">Tela inicial</string> + <string name="browser_pill_tile_other">Outro navegador</string> + <string name="browser_pill_tile_full">Tela cheia</string> + <string name="browser_pill_edit_address">Editar endereço</string> + <string name="browser_pill_back">Voltar</string> + <string name="browser_pill_forward">Avançar</string> + <string name="browser_pill_reload">Recarregar</string> + <string name="browser_pill_stop">Parar de carregar</string> + <string name="browser_pill_favorite_add">Adicionar aos favoritos</string> + <string name="browser_pill_favorite_remove">Remover dos favoritos</string> + <string name="browser_pill_share">Compartilhar</string> + <string name="browser_pill_copy">Copiar link</string> + <string name="browser_pill_find">Localizar na página</string> + <string name="browser_pill_text_size">Tamanho do texto</string> + <string name="browser_pill_add_home">Adicionar à tela inicial</string> + <string name="browser_pill_desktop">Versão para desktop</string> + <string name="browser_pill_other_browser">Abrir no navegador</string> + <string name="browser_pill_other_browser_named">Abrir no %1$s</string> + <string name="browser_pill_full_screen">Tela cheia</string> + <string name="browser_pill_left_site">Você saiu de %1$s</string> + <string name="browser_pill_back_to_app">Voltar ao app</string> + <string name="browser_pill_text_smaller">Texto menor</string> + <string name="browser_pill_text_larger">Texto maior</string> + <string name="browser_pill_text_reset">Redefinir</string> + <string name="browser_pill_text_value">%1$d%</string> + <string name="browser_pill_privacy">Privacidade</string> + <string name="browser_pill_tor_title">Roteamento onion</string> + <string name="browser_pill_tor_on">O site não consegue ver seu endereço IP</string> + <string name="browser_pill_tor_off">O site consegue ver seu endereço IP</string> + <string name="browser_pill_site_settings">Configurações do site</string> + <string name="browser_pill_site_settings_none">Nada permitido ainda</string> + <string name="browser_pill_access">O que ele pode acessar</string> + <string name="browser_pill_access_desc">Suas chaves nunca saem do Amethyst</string> + <string name="browser_pill_access_keys_desc">Toda assinatura, publicação, envio de arquivo ou pagamento passa pela sua aprovação</string> + <string name="browser_pill_access_none">Nenhum acesso especial</string> + <string name="browser_pill_access_none_desc">Ele roda isolado e não pode agir na sua conta</string> + <string name="browser_pill_access_manage">Gerenciar permissões</string> + <string name="browser_pill_access_nsite">nSite · isolado</string> + <string name="browser_pill_access_napplet">nApplet · isolado</string> + <string name="browser_pill_console">Console</string> + <string name="browser_pill_security_https">Conexão segura</string> + <string name="browser_pill_security_http">Não seguro</string> + <string name="browser_pill_security_tor">Roteado via onion</string> + <string name="browser_pill_security_sandbox">App isolado</string> + <string name="browser_pill_permission_camera">Câmera</string> + <string name="browser_pill_permission_microphone">Microfone</string> + <string name="browser_pill_permission_location">Localização</string> + <string name="browser_pill_decision_ask">Perguntar</string> + <string name="browser_pill_decision_allow">Permitir</string> + <string name="browser_pill_decision_block">Bloquear</string> + <string name="browser_pill_permission_state">%1$s: %2$s</string> + <string name="browser_pill_decision_allowed">permitido</string> + <string name="browser_pill_decision_blocked">bloqueado</string> + <string name="browser_pill_address_hint">Pesquise ou digite um endereço</string> + <string name="browser_pill_go">Ir</string> + <string name="browser_pill_clear">Limpar</string> + <string name="browser_pill_paste_go">Colar e ir</string> + <string name="browser_pill_fill_in">Usar este endereço</string> + <string name="browser_pill_find_hint">Localizar na página</string> + <string name="browser_pill_find_count">%1$d / %2$d</string> + <string name="browser_pill_find_none">Nenhuma correspondência</string> + <string name="browser_pill_find_previous">Correspondência anterior</string> + <string name="browser_pill_find_next">Próxima correspondência</string> + <string name="browser_pill_find_close">Fechar localizar na página</string> + <string name="browser_pill_console_all">Todos</string> + <string name="browser_pill_console_errors">Erros</string> + <string name="browser_pill_console_warnings">Avisos</string> + <string name="browser_pill_console_copy">Copiar</string> + <string name="browser_pill_console_clear">Limpar</string> + <string name="browser_pill_console_empty">Nada registrado ainda</string> + <string name="browser_pill_perm_title">Este site quer usar</string> + <string name="browser_pill_perm_camera_desc">Ver o que sua câmera vê</string> + <string name="browser_pill_perm_microphone_desc">Ouvir o que seu microfone ouve</string> + <string name="browser_pill_perm_location_desc">Saber aproximadamente onde você está</string> + <string name="browser_pill_perm_tor_note">Chamadas podem revelar seu endereço IP mesmo pelo Tor.</string> + <string name="browser_pill_perm_allow">Permitir durante a visita</string> + <string name="browser_pill_perm_once">Só desta vez</string> + <string name="browser_pill_perm_deny">Não permitir</string> + <string name="browser_pill_dialog_says">%1$s diz</string> + <string name="browser_pill_dialog_generic">Esta página diz</string> + <string name="browser_pill_dialog_block">Impedir que esta página mostre mais diálogos</string> + <string name="browser_pill_dialog_answer">Sua resposta</string> + <string name="browser_pill_dialog_leave_title">Sair do site?</string> + <string name="browser_pill_dialog_leave_message">As alterações que você fez podem não ser salvas.</string> + <string name="browser_pill_dialog_leave">Sair</string> + <string name="browser_pill_ok">OK</string> + <string name="browser_pill_cancel">Cancelar</string> + <string name="browser_pill_info_connection">Conexão</string> + <string name="browser_pill_info_https">A conexão é criptografada</string> + <string name="browser_pill_info_https_desc">As informações que você envia não podem ser lidas no caminho.</string> + <string name="browser_pill_info_http">A conexão não é criptografada</string> + <string name="browser_pill_info_http_desc">Não digite senhas nem dados de pagamento.</string> + <string name="browser_pill_info_tor">Roteado via onion pelo Tor</string> + <string name="browser_pill_info_open">Web aberta</string> + <string name="browser_pill_info_certificate">Certificado</string> + <string name="browser_pill_info_certificate_desc">Emitido para %1$s por %2$s · válido até %3$s</string> + <string name="browser_pill_info_permissions">Permissões</string> + <string name="browser_pill_info_site_data">Cookies e dados do site</string> + <string name="browser_pill_info_site_data_desc">Logins e preferências que este site salvou para esta conta</string> + <string name="browser_pill_info_clear">Limpar dados do site</string> + <string name="browser_pill_info_clear_confirm">Sair deste site e excluir os dados dele?</string> + <string name="backup_action_block_again">Bloquear %1$s de novo</string> + <string name="backup_action_keep_count">Manter %1$s</string> + <string name="backup_action_rejoin">Entrar de novo em %1$s</string> + <string name="backup_action_remute">Silenciar %1$s de novo</string> + <string name="backup_action_restore_count">Restaurar %1$s</string> + <string name="backup_action_restore_feeds">Restaurar feeds</string> + <string name="backup_action_restore_places">Restaurar lugares</string> + <string name="backup_action_restore_profile">Restaurar perfil</string> + <string name="backup_action_restore_providers">Restaurar provedores</string> + <string name="backup_action_restore_relays">Restaurar relays</string> + <string name="backup_action_restore_topics">Restaurar tópicos</string> + <string name="backup_action_restore_wallet">Restaurar carteira</string> + <string name="backup_card_accept">Aceitar</string> + <plurals name="backup_card_and_added"> + <item quantity="one">%1$s e adicionou %2$d</item> + <item quantity="other">%1$s e adicionou %2$d</item> + </plurals> + <plurals name="backup_card_and_blocked"> + <item quantity="one">%1$s e bloqueou %2$d</item> + <item quantity="other">%1$s e bloqueou %2$d</item> + </plurals> + <plurals name="backup_card_and_followed"> + <item quantity="one">%1$s e seguiu %2$d</item> + <item quantity="other">%1$s e seguiu %2$d</item> + </plurals> + <plurals name="backup_card_and_joined"> + <item quantity="one">%1$s e entrou em %2$d</item> + <item quantity="other">%1$s e entrou em %2$d</item> + </plurals> + <plurals name="backup_card_and_muted"> + <item quantity="one">%1$s e silenciou %2$d</item> + <item quantity="other">%1$s e silenciou %2$d</item> + </plurals> + <string name="backup_card_app_settings">Suas configurações sincronizadas do Amethyst foram alteradas em outro app que não é o Amethyst. Isso pode causar problemas.</string> + <string name="backup_card_concord_emptied">Você esvaziou sua lista do Concord</string> + <string name="backup_card_concord_rewritten">Você alterou sua lista do Concord</string> + <plurals name="backup_card_dm_relays_lost"> + <item quantity="one">Você removeu %1$d relay de DM</item> + <item quantity="other">Você removeu %1$d relays de DM</item> + </plurals> + <plurals name="backup_card_feeds_removed"> + <item quantity="one">Você removeu %1$d feed favorito</item> + <item quantity="other">Você removeu %1$d feeds favoritos</item> + </plurals> + <plurals name="backup_card_follows_dropped"> + <item quantity="one">Você deixou de seguir %1$d pessoa</item> + <item quantity="other">Você deixou de seguir %1$d pessoas</item> + </plurals> + <plurals name="backup_card_hashtags_unfollowed"> + <item quantity="one">Você deixou de seguir %1$d hashtag</item> + <item quantity="other">Você deixou de seguir %1$d hashtags</item> + </plurals> + <plurals name="backup_card_indexer_relays_lost"> + <item quantity="one">Você removeu %1$d relay de indexação</item> + <item quantity="other">Você removeu %1$d relays de indexação</item> + </plurals> + <plurals name="backup_card_key_package_relays_lost"> + <item quantity="one">Você removeu %1$d relay de chaves de chat em grupo</item> + <item quantity="other">Você removeu %1$d relays de chaves de chat em grupo</item> + </plurals> + <plurals name="backup_card_leave_chats"> + <item quantity="one">Você saiu de %1$d chat público</item> + <item quantity="other">Você saiu de %1$d chats públicos</item> + </plurals> + <plurals name="backup_card_leave_communities"> + <item quantity="one">Você saiu de %1$d comunidade</item> + <item quantity="other">Você saiu de %1$d comunidades</item> + </plurals> + <plurals name="backup_card_leave_groups"> + <item quantity="one">Você saiu de %1$d grupo</item> + <item quantity="other">Você saiu de %1$d grupos</item> + </plurals> + <plurals name="backup_card_leave_rooms"> + <item quantity="one">Você saiu de %1$d sala de chat</item> + <item quantity="other">Você saiu de %1$d salas de chat</item> + </plurals> + <plurals name="backup_card_mutes_visible"> + <item quantity="one">Você deixou de silenciar %1$d item</item> + <item quantity="other">Você deixou de silenciar %1$d itens</item> + </plurals> + <plurals name="backup_card_nip65_lost"> + <item quantity="one">Você removeu %1$d relay público</item> + <item quantity="other">Você removeu %1$d relays públicos</item> + </plurals> + <string name="backup_card_nutzap_key_removed">Você removeu sua chave de nutzap</string> + <string name="backup_card_nutzap_key_replaced">Você substituiu sua chave de nutzap</string> + <plurals name="backup_card_nutzap_mints_removed"> + <item quantity="one">Você removeu %1$d mint de nutzap</item> + <item quantity="other">Você removeu %1$d mints de nutzap</item> + </plurals> + <plurals name="backup_card_offers_removed"> + <item quantity="one">Você removeu %1$d oferta BOLT12</item> + <item quantity="other">Você removeu %1$d ofertas BOLT12</item> + </plurals> + <plurals name="backup_card_payment_targets_removed"> + <item quantity="one">Você removeu %1$d forma de receber pagamentos</item> + <item quantity="other">Você removeu %1$d formas de receber pagamentos</item> + </plurals> + <plurals name="backup_card_places_unfollowed"> + <item quantity="one">Você deixou de seguir %1$d lugar</item> + <item quantity="other">Você deixou de seguir %1$d lugares</item> + </plurals> + <plurals name="backup_card_private_outbox_lost"> + <item quantity="one">Você removeu %1$d relay privado</item> + <item quantity="other">Você removeu %1$d relays privados</item> + </plurals> + <plurals name="backup_card_profile_lost_fields"> + <item quantity="one">Você removeu %1$d campo do perfil</item> + <item quantity="other">Você removeu %1$d campos do perfil</item> + </plurals> + <plurals name="backup_card_relay_feeds_lost"> + <item quantity="one">Você removeu %1$d feed de relay</item> + <item quantity="other">Você removeu %1$d feeds de relay</item> + </plurals> + <plurals name="backup_card_search_relays_lost"> + <item quantity="one">Você removeu %1$d relay de busca</item> + <item quantity="other">Você removeu %1$d relays de busca</item> + </plurals> + <plurals name="backup_card_trust_services_changed"> + <item quantity="one">Você trocou o provedor de %1$d serviço de confiança</item> + <item quantity="other">Você trocou o provedor de %1$d serviços de confiança</item> + </plurals> + <plurals name="backup_card_trusted_lost"> + <item quantity="one">Você deixou de confiar em %1$d relay</item> + <item quantity="other">Você deixou de confiar em %1$d relays</item> + </plurals> + <plurals name="backup_card_unblocked"> + <item quantity="one">Você desbloqueou %1$d relay</item> + <item quantity="other">Você desbloqueou %1$d relays</item> + </plurals> + <string name="backup_card_wallet_emptied">Você esvaziou sua carteira Cashu</string> + <string name="backup_card_wallet_rewritten">Você alterou sua carteira Cashu</string> + <string name="backup_conflict_added">Adicionados (%1$s)</string> + <string name="backup_conflict_changed">Alterados (%1$s)</string> + <string name="backup_conflict_count_added">Adicionados: %1$s</string> + <string name="backup_conflict_count_changed">Alterados: %1$s</string> + <string name="backup_conflict_count_removed">Removidos: %1$s</string> + <string name="backup_conflict_keep_new">Usar a nova versão</string> + <string name="backup_conflict_private_added">Você adicionou itens privados (criptografados).</string> + <string name="backup_conflict_private_changed">Você também alterou itens privados (criptografados). Eles não podem ser comparados aqui.</string> + <string name="backup_conflict_private_cleared">Você removeu todos os itens privados (criptografados).</string> + <string name="backup_conflict_relay_read_only">somente leitura</string> + <string name="backup_conflict_relay_read_write">leitura e escrita</string> + <string name="backup_conflict_relay_write_only">somente escrita</string> + <string name="backup_conflict_removed">Removidos (%1$s)</string> + <string name="backup_conflict_restore_mine">Restaurar a versão salva</string> + <string name="backup_conflict_title">Alterado em outro app: %1$s</string> + <string name="backup_entry_algo_feed">Feeds</string> + <string name="backup_entry_chat_room">Salas de chat</string> + <string name="backup_entry_community">Comunidades</string> + <string name="backup_entry_hashtag">Hashtags</string> + <string name="backup_entry_identity_claim">Identidades vinculadas</string> + <string name="backup_entry_location">Localizações</string> + <string name="backup_entry_mint">Mints</string> + <string name="backup_entry_nutzap_key">Chaves de nutzap</string> + <string name="backup_entry_offer">Ofertas</string> + <string name="backup_entry_other">Outras entradas</string> + <string name="backup_entry_payment_target">Endereços de pagamento</string> + <string name="backup_entry_person">Pessoas</string> + <string name="backup_entry_profile_field">Campos do perfil</string> + <string name="backup_entry_public_chat">Chats públicos</string> + <string name="backup_entry_relay">Relays</string> + <string name="backup_entry_relay_group">Grupos</string> + <string name="backup_entry_thread">Threads</string> + <string name="backup_entry_trust_provider">Provedores de confiança</string> + <string name="backup_entry_word">Palavras</string> + <string name="backup_profile_field_about">Sobre</string> + <string name="backup_profile_field_banner">Banner</string> + <string name="backup_profile_field_birthday">Aniversário</string> + <string name="backup_profile_field_bot">Marcação de bot</string> + <string name="backup_profile_field_clink_offer">Oferta CLINK</string> + <string name="backup_profile_field_display_name">Nome de exibição</string> + <string name="backup_profile_field_lud06">LNURL</string> + <string name="backup_profile_field_lud16">Endereço Lightning</string> + <string name="backup_profile_field_name">Nome de usuário</string> + <string name="backup_profile_field_nip05">Endereço Nostr</string> + <string name="backup_profile_field_picture">Foto</string> + <string name="backup_profile_field_pronouns">Pronomes</string> + <string name="backup_profile_field_website">Site</string> + <string name="backup_review_allowed_tag">Permitido</string> + <string name="backup_review_changed_by_other_app">Alterado em outro app · %1$s</string> + <string name="backup_review_concord_body">Suas comunidades Concord são criptografadas para você, por isso não podem ser comparadas aqui. A restauração devolve exatamente o que este dispositivo salvou.</string> + <string name="backup_review_demoted_tag">Movido</string> + <string name="backup_review_dropped_count">Removidos: %1$s</string> + <string name="backup_review_dropped_tag">Removido</string> + <string name="backup_review_encrypted_added">Você criou</string> + <string name="backup_review_encrypted_changed">Você alterou</string> + <string name="backup_review_encrypted_cleared">Você esvaziou</string> + <string name="backup_review_feed">Feed</string> + <string name="backup_review_gone">Removido</string> + <string name="backup_review_hashtags_you_follow">Hashtags que você segue</string> + <string name="backup_review_inbox">Caixa de entrada</string> + <string name="backup_review_inbox_explainer">onde os outros encontram você</string> + <string name="backup_review_joined">entradas</string> + <string name="backup_review_kept">mantidos</string> + <string name="backup_review_kept_count">Mantidos: %1$s</string> + <string name="backup_review_key_removed">Chave de recebimento removida</string> + <string name="backup_review_key_replaced">Chave de recebimento substituída</string> + <string name="backup_review_key_warning">Os nutzaps ficarão bloqueados para esta nova chave. Se este dispositivo não a tiver, talvez você não consiga resgatá-los aqui.</string> + <string name="backup_review_left">saídas</string> + <string name="backup_review_left_tag">Saiu</string> + <string name="backup_review_listed_tag">Listado</string> + <string name="backup_review_moved_tag">Movido</string> + <string name="backup_review_names_and_more">%1$s e mais %2$s</string> + <string name="backup_review_new">Nova versão</string> + <string name="backup_review_new_count">Novos: %1$s</string> + <string name="backup_review_new_count_caption">novos</string> + <string name="backup_review_new_tag">Novo</string> + <string name="backup_review_newly_blocked">Bloqueados agora</string> + <string name="backup_review_newly_muted">Silenciados agora: %1$s</string> + <string name="backup_review_no_longer_blocked">Não mais bloqueados</string> + <string name="backup_review_not_set">Não definido</string> + <string name="backup_review_nothing_here">Nada aqui</string> + <string name="backup_review_offer">Oferta BOLT12</string> + <plurals name="backup_review_offers_gone"> + <item quantity="one">%1$d oferta removida</item> + <item quantity="other">%1$d ofertas removidas</item> + </plurals> + <string name="backup_review_outbox">Caixa de saída</string> + <string name="backup_review_outbox_explainer">onde você publica</string> + <plurals name="backup_review_payment_targets_gone"> + <item quantity="one">%1$d forma de pagar você removida</item> + <item quantity="other">%1$d formas de pagar você removidas</item> + </plurals> + <string name="backup_review_payments_explainer">Quem paga você só vê o que está nesta lista.</string> + <string name="backup_review_places_gone">lugares removidos</string> + <string name="backup_review_places_you_follow">Lugares que você segue</string> + <string name="backup_review_private_added_body">Agora eles estão criptografados, e provavelmente foi para lá que os itens públicos acima foram.</string> + <string name="backup_review_private_added_title">Você tornou itens privados</string> + <string name="backup_review_private_changed_body">Eles são criptografados, por isso não podem ser comparados aqui.</string> + <string name="backup_review_private_changed_title">Você alterou itens privados</string> + <string name="backup_review_private_wiped_body">Eles são criptografados, então só você pode ver o que eram. A restauração traz todos de volta.</string> + <string name="backup_review_private_wiped_title">Você removeu todos os itens privados</string> + <string name="backup_review_receiving_tag">Recebimento</string> + <string name="backup_review_renamed">renomeados</string> + <string name="backup_review_renamed_tag">Renomeado</string> + <string name="backup_review_saved">Neste dispositivo</string> + <plurals name="backup_review_search_people"> + <item quantity="one">Pesquisar %1$s pessoa</item> + <item quantity="other">Pesquisar entre %1$s pessoas</item> + </plurals> + <string name="backup_review_searching_tag">Busca</string> + <string name="backup_review_service">Serviço</string> + <string name="backup_review_service_followers">Seguidores</string> + <string name="backup_review_service_rank">Classificação</string> + <string name="backup_review_service_topics">Tópicos</string> + <plurals name="backup_review_services_changed"> + <item quantity="one">%1$s de %2$s serviço alterado</item> + <item quantity="other">%1$s de %2$s serviços alterados</item> + </plurals> + <string name="backup_review_services_changed_explainer">Agora eles vêm de outra pessoa, ou de ninguém</string> + <string name="backup_review_still_blocked">Ainda bloqueados</string> + <string name="backup_review_storing_tag">Armazenamento</string> + <string name="backup_review_tab_dropped">Removidos · %1$s</string> + <string name="backup_review_tab_edited">Editados · %1$s</string> + <string name="backup_review_tab_new">Novos · %1$s</string> + <string name="backup_review_title_cleared">%1$s: tudo removido</string> + <string name="backup_review_title_updated">%1$s: atualização</string> + <string name="backup_review_topics_gone">tópicos removidos</string> + <string name="backup_review_trust_warning">As classificações, as contagens de seguidores e o filtro de spam que você vê vêm desses provedores.</string> + <string name="backup_review_trusted_tag">Confiável</string> + <string name="backup_review_unblocked">Desbloqueados: %1$s</string> + <string name="backup_review_unblocked_explainer">O Amethyst vai se conectar a eles novamente</string> + <string name="backup_review_units_tag">Unidades</string> + <string name="backup_review_unmuted">Não silenciados: %1$s</string> + <string name="backup_review_unmuted_explainer">Eles podem voltar a aparecer nos seus feeds</string> + <string name="backup_review_untrusted_tag">Não confiável</string> + <string name="backup_review_wallet_body">Os mints e as chaves da sua carteira são criptografados para você, por isso não podem ser comparados item a item aqui. A restauração devolve exatamente o que este dispositivo salvou.</string> + <string name="backup_review_was_named">antes: “%1$s”</string> + <string name="backup_review_words_and_hashtags">Palavras e hashtags</string> + <string name="backup_type_app_settings">configurações sincronizadas do app</string> + <string name="backup_type_app_settings_explainer">Configurações do Amethyst sincronizadas entre seus dispositivos. Elas são criptografadas, então só é possível comparar se existem.</string> + <string name="backup_type_blocked_relays">lista de relays bloqueados</string> + <string name="backup_type_blocked_relays_explainer">Relays aos quais este app nunca se conecta.</string> + <string name="backup_type_bolt12_offers">ofertas BOLT12</string> + <string name="backup_type_bolt12_offers_explainer">As ofertas Lightning que os outros usam para pagar você.</string> + <string name="backup_type_cashu_wallet">carteira Cashu</string> + <string name="backup_type_cashu_wallet_explainer">As configurações e os mints da sua carteira de ecash. Eles são criptografados, então só é possível comparar se existem.</string> + <string name="backup_type_communities">lista de comunidades</string> + <string name="backup_type_communities_explainer">As comunidades em que você entrou.</string> + <string name="backup_type_concord_communities">lista de comunidades Concord</string> + <string name="backup_type_concord_communities_explainer">As comunidades Concord em que você entrou. Elas são criptografadas, então só é possível comparar se existem.</string> + <string name="backup_type_dm_relays">lista de relays de DM</string> + <string name="backup_type_dm_relays_explainer">Onde os outros entregam suas mensagens privadas.</string> + <string name="backup_type_ephemeral_chats">lista de chats efêmeros</string> + <string name="backup_type_ephemeral_chats_explainer">As salas de chat efêmeras em que você entrou.</string> + <string name="backup_type_favorite_algo_feeds">lista de feeds favoritos</string> + <string name="backup_type_favorite_algo_feeds_explainer">Os feeds algorítmicos que você salvou.</string> + <string name="backup_type_follow_list">lista de seguidos</string> + <string name="backup_type_follow_list_explainer">Define de quem são as publicações que aparecem no seu feed inicial.</string> + <string name="backup_type_geohashes">localizações seguidas</string> + <string name="backup_type_geohashes_explainer">Localizações cujas publicações aparecem nos seus feeds.</string> + <string name="backup_type_hashtags">hashtags seguidas</string> + <string name="backup_type_hashtags_explainer">Hashtags cujas publicações aparecem nos seus feeds.</string> + <string name="backup_type_indexer_relays">lista de relays de indexação</string> + <string name="backup_type_indexer_relays_explainer">Os relays usados para encontrar perfis e listas de relays de outras pessoas.</string> + <string name="backup_type_key_package_relays">relays de chaves de chat em grupo</string> + <string name="backup_type_key_package_relays_explainer">Onde os outros encontram as chaves para adicionar você a chats em grupo criptografados.</string> + <string name="backup_type_mute_list">lista de silenciados</string> + <string name="backup_type_mute_list_explainer">Oculta pessoas, palavras, hashtags e threads de tudo o que você vê.</string> + <string name="backup_type_nutzap_info">configurações de recebimento de nutzap</string> + <string name="backup_type_nutzap_info_explainer">Os mints e a chave que os outros usam para enviar nutzaps para você.</string> + <string name="backup_type_other">lista</string> + <string name="backup_type_other_explainer">Uma lista salva neste dispositivo.</string> + <string name="backup_type_outbox_inbox_relays">lista de relays públicos</string> + <string name="backup_type_outbox_inbox_relays_explainer">Informa a todos onde encontrar suas publicações e para onde enviar respostas e menções a você.</string> + <string name="backup_type_payment_targets">endereços de pagamento</string> + <string name="backup_type_payment_targets_explainer">As formas pelas quais outras pessoas podem pagar você.</string> + <string name="backup_type_private_outbox_relays">lista de relays privados</string> + <string name="backup_type_private_outbox_relays_explainer">Onde seus rascunhos e dados privados são armazenados.</string> + <string name="backup_type_profile">perfil</string> + <string name="backup_type_profile_explainer">Seu nome, foto, bio e endereços de pagamento, como todos os veem.</string> + <string name="backup_type_public_chats">lista de chats públicos</string> + <string name="backup_type_public_chats_explainer">Os chats públicos em que você entrou.</string> + <string name="backup_type_relay_feeds">feeds de relays favoritos</string> + <string name="backup_type_relay_feeds_explainer">Os relays que você navega como feeds.</string> + <string name="backup_type_relay_groups">lista de grupos</string> + <string name="backup_type_relay_groups_explainer">Os grupos baseados em relays em que você entrou.</string> + <string name="backup_type_search_relays">lista de relays de busca</string> + <string name="backup_type_search_relays_explainer">Os relays usados quando você pesquisa.</string> + <string name="backup_type_trust_providers">lista de provedores de confiança</string> + <string name="backup_type_trust_providers_explainer">Os serviços em que você confia para classificar e verificar pessoas para você.</string> + <string name="backup_type_trusted_relays">lista de relays confiáveis</string> + <string name="backup_type_trusted_relays_explainer">Relays em que você confia para se conectar sem perguntar.</string> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-sv-rSE/strings.xml b/commonsUI/src/commonMain/composeResources/values-sv-rSE/strings.xml index 64cf4922d4..6a44e20832 100644 --- a/commonsUI/src/commonMain/composeResources/values-sv-rSE/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-sv-rSE/strings.xml @@ -4922,4 +4922,1174 @@ one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> + <string name="geocache_unnamed">Geocache</string> + <string name="geocache_type_traditional">Traditionell</string> + <string name="geocache_type_multi">Multicache</string> + <string name="geocache_type_mystery">Mystery</string> + <string name="geocache_size_micro">Mikro</string> + <string name="geocache_size_small">Liten</string> + <string name="geocache_size_regular">Normal</string> + <string name="geocache_size_large">Stor</string> + <string name="geocache_size_other">Annan storlek</string> + <string name="geocache_difficulty_short">D%1$d</string> + <string name="geocache_terrain_short">T%1$d</string> + <string name="geocache_first_to_find">Först att hitta (FTF)</string> + <string name="geocache_is_art">Konstverk</string> + <string name="geocache_mission">Key Quest</string> + <string name="geocache_needs_verification">Verifierade fynd</string> + <string name="geocache_claimed">Tagen</string> + <string name="geocache_archived">Arkiverad</string> + <string name="geocache_hint_tap_to_reveal">Kört fast? Tryck för att visa ledtråden</string> + <string name="geocache_hint_other_reading">Går den inte att läsa? Tryck för den andra versionen</string> + <string name="geocache_nearby">I närheten</string> + <string name="geocache_found_it">Hittad!</string> + <string name="geocache_verified_find">Verifierat fynd</string> + <string name="geocache_invalid_proof">Beviset stämmer inte</string> + <string name="geocache_loading">Läser in cache…</string> + <string name="geocache_hidden_by">Gömd av %1$s</string> + <string name="geocache_navigate">Navigera</string> + <string name="geocache_copy_coordinates">Kopiera koordinater</string> + <string name="geocache_coordinates_copied">Koordinater kopierade</string> + <string name="geocache_photos_section">Foton</string> + <string name="geocache_mission_section">Key Quest-uppdrag</string> + <string name="geocache_logs_section">Loggar</string> + <string name="geocache_no_logs">Ingen har loggat den här cachen ännu. Bli först.</string> + <string name="geocache_i_found_it">Jag hittade den</string> + <string name="geocache_didnt_find_it">Hittade den inte</string> + <string name="geocache_add_note">Lägg till en anteckning</string> + <string name="geocache_needs_maintenance">Rapportera behov av underhåll</string> + <string name="geocache_add_to_hunt">Lägg till i en jakt</string> + <string name="geocache_hunt_new">Ny jakt…</string> + <string name="geocache_hunt_none_yet">Du har inte skapat någon jakt ännu.</string> + <string name="geocache_hunt_already_on">Redan med i den här jakten</string> + <plurals name="geocache_caches_here"> + <item quantity="one">%1$d cache här</item> + <item quantity="other">%1$d cacher här</item> + </plurals> + <string name="geocache_photo_remove">Ta bort det här fotot</string> + <string name="geocache_archived_notice">Den här cachen har arkiverats. Den är inte längre i spel.</string> + <string name="geocache_ftf_won_by">FTF tagen av %1$s</string> + <string name="geocache_ftf_available">FTF är fortfarande ledig</string> + <string name="geocache_owner_actions">Ägare</string> + <string name="geocache_owner_edit">Redigera den här cachen</string> + <string name="geocache_owner_archive">Arkivera den här cachen</string> + <string name="geocache_owner_archive_confirm">Arkivera den. Hittare ser att den är ur spel, och beskrivningen går fortfarande att läsa.</string> + <string name="geocache_owner_lock_ftf">Lås den första hittaren</string> + <string name="geocache_owner_lock_ftf_explain">Publicerar en F-tagg som anger %1$s. Detta är permanent.</string> + <string name="geocache_owner_no_finders">Inga verifierade fynd ännu, så det finns ingen att låsa.</string> + <string name="geocache_owner_show_qr">Visa verifieringskoden</string> + <string name="geocache_owner_qr_warning">Skriv ut den och lägg den i cachen. Amethyst sparar den inte — om du tappar bort den kan hittare inte längre bevisa att de var där.</string> + <string name="geocache_log_type_dnf">Hittade inte (DNF)</string> + <string name="geocache_log_type_note">Anteckning</string> + <string name="geocache_log_type_maintenance">Behöver underhåll</string> + <string name="geocache_log_title">Logga ett fynd</string> + <string name="geocache_log_cache_label">Cache</string> + <string name="geocache_log_your_log">Din logg</string> + <string name="geocache_log_placeholder">Hur gick det?</string> + <string name="geocache_log_add_photo">Lägg till ett foto</string> + <string name="geocache_log_mission_answer">Ditt Key Quest-svar</string> + <string name="geocache_log_proof_section">Bevis på närvaro</string> + <string name="geocache_log_scan_code">Skanna koden i cachen</string> + <string name="geocache_log_scan_again">Skanna en annan kod</string> + <string name="geocache_log_proof_verified">Verifierat — du var vid cachen</string> + <string name="geocache_log_proof_explain">Signeras en gång med koden i lådan. Nyckeln sparas aldrig och lämnar aldrig den här skärmen.</string> + <string name="geocache_log_proof_bad_code">Det där är ingen verifieringskod.</string> + <string name="geocache_log_proof_wrong_cache">Den koden hör till en annan cache.</string> + <string name="geocache_log_proof_optional">Den här cachen kräver inget bevis, så du kan logga den direkt.</string> + <string name="geocache_log_no_location_attached">Din egen plats bifogas inte till den här loggen.</string> + <string name="geocache_log_post">Publicera</string> + <string name="geocache_log_comment_title">Lägg till en logg</string> + <string name="geocache_tab_nearby">I närheten</string> + <string name="geocache_tab_map">Karta</string> + <string name="geocache_tab_hunts">Jakter</string> + <string name="geocache_tab_finds">Fynd</string> + <string name="geocache_tab_mine">Mina</string> + <string name="geocache_filter_unfound">Ej hittade</string> + <string name="geocache_filter_difficulty">D ≤ %1$d</string> + <string name="geocache_filter_terrain">T ≤ %1$d</string> + <string name="geocache_filter_has_prize">Har ett pris</string> + <string name="geocache_filter_verified_only">Bevis krävs</string> + <string name="geocache_use_my_location">Använd min plats</string> + <string name="geocache_sorted_by_distance">Sorterat efter avstånd</string> + <string name="geocache_sorted_by_recent">Nyaste först</string> + <string name="geocache_hide_a_cache">Göm en cache</string> + <string name="geocache_empty_nearby">Inga cacher här ännu. Prova en bredare följlista, eller göm den första.</string> + <string name="geocache_empty_finds">Du har inte loggat något fynd ännu.</string> + <string name="geocache_empty_mine">Du har inte gömt någon cache ännu.</string> + <string name="geocache_empty_hunts">Inga jakter ännu.</string> + <string name="geocache_stat_finds">Fynd</string> + <string name="geocache_stat_ftf">FTF</string> + <string name="geocache_stat_verified">Verifierade</string> + <string name="geocache_stat_hidden">Gömda</string> + <string name="geocache_map_recenter">Centrera på mig</string> + <string name="geocache_map_long_press">Tryck länge på kartan för att gömma en cache där</string> + <string name="geocache_map_open">Öppna</string> + <string name="geocache_new_section_where">Var</string> + <string name="geocache_new_section_what">Vad</string> + <string name="geocache_new_section_extras">Extra</string> + <string name="geocache_new_section_proof">Bevis</string> + <string name="geocache_new_section_relays">Reläer</string> + <string name="geocache_new_location_warning">Detta publicerar en offentlig plats med cirka 5 meters noggrannhet. Alla på Nostr kan se den, och en publicerad händelse kan inte återkallas.</string> + <string name="geocache_new_pick_location">Välj platsen</string> + <string name="geocache_new_ladder">Publicerar %1$s, plus grövre celler ned till 3 tecken för närhetssökning.</string> + <string name="geocache_new_name">Namn</string> + <string name="geocache_new_description">Beskrivning</string> + <string name="geocache_new_type">Typ</string> + <string name="geocache_new_size">Storlek</string> + <string name="geocache_new_difficulty">Svårighetsgrad</string> + <string name="geocache_new_terrain">Terräng</string> + <string name="geocache_new_hint">Ledtråd</string> + <string name="geocache_new_hint_preview">Hittare kommer att se</string> + <string name="geocache_new_hint_scramble">Kryptera ledtråden (ROT13)</string> + <string name="geocache_new_mission">Key Quest-uppdrag</string> + <string name="geocache_new_modifiers">Tillägg</string> + <string name="geocache_new_require_proof">Kräv bevis på närvaro</string> + <string name="geocache_new_proof_ready">En verifieringsnyckel har skapats för den här cachen.</string> + <string name="geocache_new_log_relays">Var hittare ska logga</string> + <string name="geocache_new_publish">Publicera</string> + <string name="geocache_new_needs_name">Ge cachen ett namn.</string> + <string name="geocache_new_needs_location">Välj var cachen är gömd.</string> + <string name="geocache_hunt_progress">%1$d av %2$d hittade</string> + <string name="geocache_hunt_start">Starta jakten</string> + <plurals name="geocache_hunt_caches"> + <item quantity="one">%1$d cache</item> + <item quantity="other">%1$d cacher</item> + </plurals> + <string name="geocache_hunt_title">Titel</string> + <string name="geocache_hunt_description">Beskrivning</string> + <string name="geocache_hunt_banner">Bannerbild</string> + <string name="geocache_hunt_pick_caches">Cacher, i ordning</string> + <string name="geocache_hunt_add_cache">Lägg till en cache</string> + <string name="geocache_hunt_move_up">Flytta upp</string> + <string name="geocache_hunt_move_down">Flytta ned</string> + <string name="geocache_hunt_remove">Ta bort</string> + <string name="geocache_hunt_theme">Sidtema</string> + <string name="geocache_hunt_map_style">Kartstil</string> + <string name="geocache_hunt_needs_title">Ge jakten en titel.</string> + <string name="geocache_hunt_needs_caches">Lägg till minst en cache.</string> + <string name="geocache_publish_failed">Kunde inte publicera. Kontrollera reläanslutningen och försök igen.</string> + <string name="cordn_groups_title">Cordn-grupper</string> + <string name="cordn_badge_coordinator">Koordinatorgrupp</string> + <string name="cordn_badge_coordinator_desc">Levereras av en koordinator, inte av reläer</string> + <string name="cordn_exposure_title">Vad den här koordinatorn kan se</string> + <string name="cordn_exposure_subtitle">En koordinator är en enda server som för vidare varje meddelande i den här gruppen, i ordning. Det är en annan avvägning än med reläer — värt att känna till innan du behandlar det här som vilken chatt som helst.</string> + <string name="cordn_exposure_dimension_content">Meddelandeinnehåll</string> + <string name="cordn_exposure_dimension_membership">Vilka som är med i gruppen</string> + <string name="cordn_exposure_dimension_messaging">Vem som skickar vad</string> + <string name="cordn_exposure_level_none">Oläsligt</string> + <string name="cordn_exposure_level_pseudonymous">Under en engångsnyckel</string> + <string name="cordn_exposure_level_identified">Kopplat till ditt riktiga konto</string> + <string name="cordn_exposure_details">Detaljer</string> + <string name="cordn_exposure_coordinator">Koordinator %1$s</string> + <string name="cordn_exposure_differs">Detta är inte samma exponering som i en Marmot-grupp, där ingen enskild operatör har hela konversationen.</string> + <string name="cordn_note_membership">När du går med namnges riktiga konton i båda ändar, så koordinatorn vet vilka som är med i gruppen.</string> + <string name="cordn_note_linked">En och samma engångsnyckel skickar och hämtar för alla dina grupper här, så koordinatorn kan se att allihop är du.</string> + <string name="cordn_note_history">En enda operatör har den fullständiga, ordnade historiken för varje grupp den för vidare.</string> + <string name="cordn_note_publication">Nyckelpaketet du publicerade är ett signerat belägg för att det här kontot använder cordn, och koordinatorn kan lämna ut det till vem som helst.</string> + <string name="cordn_note_sizes">Meddelanden utfylls inte, så koordinatorn ser hur långt varje meddelande är.</string> + <string name="cordn_note_encryption_bug">Den här klienten krypterar inte till koordinatorn. Det är en bugg — rapportera den gärna.</string> + <string name="cordn_health_unknown">Inte kontaktad ännu</string> + <string name="cordn_health_ok">Svarar</string> + <string name="cordn_health_down">Svarar inte</string> + <plurals name="cordn_health_failures"> + <item quantity="one">%1$d misslyckat försök i rad</item> + <item quantity="other">%1$d misslyckade försök i rad</item> + </plurals> + <string name="cordn_link_explainer">Klistra in en cordn1…-länk som någon delat med dig för att se vilken koordinator som för vidare gruppen, och vad den operatören skulle få veta om dig om du gick med.</string> + <string name="cordn_link_field">cordn1…</string> + <string name="cordn_link_inspect">Granska</string> + <string name="cordn_link_paste">Klistra in</string> + <string name="cordn_link_clear">Rensa</string> + <string name="cordn_link_invalid">Det där är ingen cordn-grupplänk: %1$s</string> + <string name="cordn_link_group_id">Grupp-id</string> + <string name="cordn_link_coordinator">Koordinator</string> + <string name="cordn_link_relays">Nås via</string> + <string name="cordn_link_no_coordinator">Den här länken anger ingen koordinator, så den kan inte följas på egen hand. Fråga den som skickade den vilken koordinator som för vidare gruppen.</string> + <string name="cordn_link_not_joinable">Att granska en länk går inte med i något, och att ha en gör dig inte till medlem.</string> + <string name="cordn_link_title">Cordn-grupplänk</string> + <string name="cordn_link_search_keywords">cordn koordinator grupp länk inbjudan mls chatt metadata integritet exponering coordinator group link invite</string> + <string name="cordn_coordinators_title">Cordn-koordinatorer</string> + <string name="cordn_coordinators_search_keywords">cordn koordinator lägg till ta bort rensa hälsa server mls chatt ordning coordinator add remove purge health</string> + <string name="cordn_keypackages_title">Cordn-nyckelpaket</string> + <string name="cordn_keypackages_search_keywords">cordn nyckelpaket publicera inbjudan reserv koordinator mls key package last resort</string> + <string name="cordn_backup_title">Cordn-säkerhetskopia</string> + <string name="cordn_backup_search_keywords">cordn säkerhetskopia återställ exportera importera lösenfras grupper återställning backup restore</string> + <string name="cordn_group_untitled">Grupp %1$s</string> + <string name="cordn_group_no_messages_yet">Inga meddelanden ännu</string> + <string name="cordn_group_via_coordinator">Cordn-grupp, förs vidare av %1$s</string> + <string name="cordn_preview_deleted">Meddelandet raderat</string> + <string name="cordn_preview_voice_note">Röstmeddelande</string> + <string name="cordn_preview_photo">Foto</string> + <string name="cordn_preview_video">Video</string> + <string name="cordn_preview_file">Fil</string> + <string name="cordn_invitations_entry">Inbjuden till en cordn-grupp?</string> + <string name="cordn_invitations_entry_action">Granska inbjudningar</string> + <string name="cordn_hub_title">Cordn</string> + <string name="cordn_hub_explainer">Grupper som levereras av en koordinator du väljer, i stället för av reläer.</string> + <string name="cordn_hub_link">Granska en grupplänk</string> + <string name="cordn_hub_link_desc">Se vad en cordn1…-länk pekar på innan du agerar på den</string> + <string name="cordn_migrate_section_send">Flytta från den här telefonen</string> + <string name="cordn_migrate_section_receive">Ta över från en annan telefon</string> + <string name="cordn_keypackages_section">Publicerade nyckelpaket</string> + <string name="cordn_link_section_input">Länken</string> + <string name="cordn_coordinators_section_yours">Koordinatorer du använder</string> + <string name="cordn_coordinators_section_discover">Annonserar sig själva</string> + <string name="cordn_coordinators_section_manual">Lägg till en manuellt</string> + <string name="cordn_backup_section_passphrase">Lösenfras</string> + <string name="cordn_backup_section_export">Skapa en säkerhetskopia</string> + <string name="cordn_backup_section_restore">Återställ från en säkerhetskopia</string> + <string name="cordn_hub_section_service">Koordinatorn</string> + <string name="cordn_hub_section_device">Den här enheten</string> + <string name="cordn_hub_coordinators">Koordinatorer</string> + <string name="cordn_hub_coordinators_desc">Servrarna som för vidare dina grupper</string> + <string name="cordn_hub_keypackages">Nyckelpaket</string> + <string name="cordn_hub_keypackages_desc">Det som gör att andra kan lägga till dig i en grupp</string> + <string name="cordn_hub_backup">Säkerhetskopiera och återställ</string> + <string name="cordn_hub_backup_desc">En lösenfraskrypterad fil som du behåller</string> + <string name="cordn_hub_migrate">Flytta till en ny telefon</string> + <string name="cordn_hub_migrate_desc">Lämna över dina grupper till en enhet du byter till</string> + <string name="cordn_migrate_title">Flytta till en ny telefon</string> + <string name="cordn_migrate_explainer">Dina cordn-grupper finns på den här enheten. Att flytta dem kräver en kod: den här telefonen publicerar dem, den nya telefonen läser dem, och den här telefonen slutar.</string> + <string name="cordn_migrate_sign_in_first">Logga först in med samma konto på den nya telefonen. Detta flyttar dina grupper, aldrig din nyckel.</string> + <string name="cordn_migrate_export">Starta överlämningen</string> + <string name="cordn_migrate_exporting">Publicerar…</string> + <string name="cordn_migrate_scan_this">Skanna den här på den nya telefonen</string> + <string name="cordn_migrate_code_note">Koden är bara en pekare. Den som fotograferar den får inte veta något — det den pekar på är krypterat till dig.</string> + <string name="cordn_migrate_handed_off">Den här enheten har lämnat över sina grupper</string> + <string name="cordn_migrate_handed_off_desc">Den har slutat skicka och ta emot, eftersom två telefoner som skickar från samma grupp skulle förstöra den för alla i den. Dina grupper finns kvar på den här enheten och inget har raderats.</string> + <string name="cordn_migrate_cancel">Ta tillbaka den här enheten</string> + <string name="cordn_migrate_cancel_desc">Använd detta om flytten inte blev klar. Gör det bara om den nya telefonen inte har börjat skicka.</string> + <string name="cordn_migrate_receive">Ta emot från en annan telefon</string> + <string name="cordn_migrate_scan">Skanna koden</string> + <string name="cordn_migrate_paste">eller klistra in den</string> + <string name="cordn_migrate_import">Hämta grupperna hit</string> + <string name="cordn_migrate_importing">Hämtar…</string> + <string name="cordn_migrate_replaces_warning">Detta ersätter alla cordn-grupper som redan finns på den här telefonen. De kan inte slås samman.</string> + <plurals name="cordn_migrate_done"> + <item quantity="one">%1$d grupp flyttad</item> + <item quantity="other">%1$d grupper flyttade</item> + </plurals> + <string name="cordn_migrate_no_groups">Det finns inga cordn-grupper på den här enheten att flytta.</string> + <string name="cordn_migrate_no_server">Ställ in en mediaserver först — de krypterade dokumenten behöver någonstans att ligga medan den andra telefonen hämtar dem.</string> + <string name="cordn_migrate_exposure_title">Vad som lämnar den här enheten</string> + <string name="cordn_migrate_exposure_body">Ditt grupptillstånd krypteras och laddas upp till din mediaserver så att den andra telefonen kan hämta det. Det är oläsligt för servern, som bara ser en storlek och en tidpunkt. Radera blobbarna efteråt om du inte vill lämna något kvar.</string> + <string name="cordn_action_delete">Radera</string> + <string name="cordn_action_edit">Redigera</string> + <string name="cordn_action_editing">Redigerar ditt meddelande</string> + <string name="cordn_action_go_to_message">Gå till meddelandet</string> + <string name="cordn_action_pin">Fäst</string> + <string name="cordn_action_reply">Svara</string> + <string name="cordn_action_unpin">Lossa</string> + <string name="cordn_admin_action_failed">Ändringen kunde inte genomföras.</string> + <string name="cordn_admin_coordinator_silent">Koordinatorn svarade inte, så inget ändrades. Försök igen om en stund.</string> + <string name="cordn_admin_no_key_package">%1$s har inte publicerat något nyckelpaket till den här koordinatorn, så de kan inte läggas till ännu. Be dem att först öppna cordn på den här koordinatorn.</string> + <string name="cordn_admin_no_self_remove">Du kan inte ta bort dig själv från en cordn-grupp.</string> + <string name="cordn_admin_no_welcome">%1$s kunde inte ges någon väg in i gruppen, så tillägget avbröts.</string> + <string name="cordn_admin_not_a_member">%1$s är inte med i den här gruppen.</string> + <string name="cordn_admin_someone">Den personen</string> + <string name="cordn_admin_wrong_key_package">Den här koordinatorn lämnade ut ett nyckelpaket som tillhör någon annan, så %1$s lades inte till. Inget ändrades.</string> + <string name="cordn_backup_contents_body">Dina gruppkrypteringsnycklar, dina nyckelpaket och varje meddelande i de grupperna. Den som öppnar filen kan läsa konversationerna. Lösenfrasen är det enda som skyddar den när den väl lämnat Amethyst.</string> + <string name="cordn_backup_contents_title">Vad filen innehåller</string> + <string name="cordn_backup_explainer">Din nsec kan inte återskapa cordn-grupper. En cordn-grupp lever som krypteringstillstånd på den här enheten och som en ordnad ström hos en koordinator som inte kan läsa den — förlorar du enheten utan säkerhetskopia är gruppen borta, inklusive allt som redan sagts i den.</string> + <string name="cordn_backup_export">Exportera</string> + <string name="cordn_backup_exported">Säkerhetskopian sparad.</string> + <string name="cordn_backup_failed">Det fungerade inte. Kontrollera lösenfrasen och filen.</string> + <string name="cordn_backup_passphrase">Lösenfras</string> + <string name="cordn_backup_restore">Återställ</string> + <string name="cordn_backup_restore_body">Återställning ersätter cordn-grupperna på den här enheten med dem i filen. Den är till för en telefon som tagit över från en annan — inte ett sätt att använda samma grupper på två enheter samtidigt.</string> + <string name="cordn_backup_restore_confirm_body">Alla cordn-grupper som nu finns på den här enheten tas bort och ersätts med dem i filen.\n\nÅterställ inte till en andra telefon medan den första fortfarande används. Båda skulle ha samma gruppnycklar, och så snart båda har skickat ett meddelande går gruppen sönder för alla — tyst, och utan väg tillbaka.</string> + <string name="cordn_backup_restore_confirm_title">Ersätta den här enhetens cordn-grupper?</string> + <string name="cordn_backup_restore_title">Återställ</string> + <string name="cordn_backup_restored">Återställd.</string> + <string name="cordn_chat_empty_description">Allt som skickas här krypteras för den här gruppen. Koordinatorn förmedlar det utan att kunna läsa det.</string> + <string name="cordn_chat_empty_title">Inga meddelanden ännu</string> + <string name="cordn_chat_unread_divider">Nya meddelanden</string> + <string name="cordn_chat_yesterday">I går</string> + <string name="cordn_composer_hint">Meddelande</string> + <string name="cordn_coordinators_add_action">Lägg till</string> + <string name="cordn_coordinators_add_failed">Kunde inte öppna den koordinatorn.</string> + <string name="cordn_coordinators_copy_key">Kopiera koordinatorns npub</string> + <string name="cordn_coordinators_discover_action">Leta efter koordinatorer</string> + <string name="cordn_coordinators_discover_add">Lägg till</string> + <string name="cordn_coordinators_discover_explainer">Läser annonser som dina reläer redan har. Ingen koordinator kontaktas, så ingen av dem får veta att du letade.</string> + <string name="cordn_coordinators_discover_failed">Kunde inte läsa annonser</string> + <string name="cordn_coordinators_discover_none">Ingen annonserar på dina reläer.</string> + <string name="cordn_coordinators_discover_seen">Annonserade för %1$s sedan</string> + <string name="cordn_coordinators_discover_seen_on">Senast annonserad %1$s</string> + <plurals name="cordn_coordinators_discover_unheard"> + <item quantity="one">Inget hittades, och %1$d av dina reläer svarade inte.</item> + <item quantity="other">Inget hittades, och %1$d av dina reläer svarade inte.</item> + </plurals> + <string name="cordn_coordinators_explainer">En koordinator ordnar en grupps meddelanden och är det enda som kan göra det. Förlorar man den förloras ordningen, och en andra koordinator speglar inte den första — så det här är namngivna saker du väljer, inte utbytbara reläer.</string> + <plurals name="cordn_coordinators_health_down"> + <item quantity="one">Svarar inte — %1$d anrop i rad har misslyckats. Grupper på den synkroniseras inte.</item> + <item quantity="other">Svarar inte — %1$d anrop i rad har misslyckats. Grupper på den synkroniseras inte.</item> + </plurals> + <string name="cordn_coordinators_health_ok">Svarar.</string> + <string name="cordn_coordinators_health_retrying">Ett anrop misslyckades; försöker igen.</string> + <string name="cordn_coordinators_health_unknown">Inget har efterfrågats ännu.</string> + <string name="cordn_coordinators_hide_older">Dölj dem som slutat annonsera</string> + <string name="cordn_coordinators_identify">Fråga vem den är</string> + <string name="cordn_coordinators_key">Koordinatornyckel</string> + <string name="cordn_coordinators_label">Ett namn för den (ditt eget, valfritt)</string> + <string name="cordn_coordinators_label_note">Det här namnet är ditt och stannar på den här enheten. En koordinator kan inte bevisa ett namn, så den ombeds aldrig uppge något.</string> + <string name="cordn_coordinators_more">Fler åtgärder</string> + <string name="cordn_coordinators_none">Inga koordinatorer ännu.</string> + <string name="cordn_coordinators_purge">Rensa bort</string> + <string name="cordn_coordinators_purge_body">Att ta bort behåller grupperna på den här enheten, så om du lägger till koordinatorn igen kommer de tillbaka. Att rensa bort raderar dem: gruppnycklarna, lässtatusen och dina nyckelpaket försvinner, och de grupperna kan bara nås igen via en ny inbjudan.\n\nKoordinatorn meddelas inte och behåller allt den redan hade.</string> + <string name="cordn_coordinators_purge_title">Rensa bort den här koordinatorn?</string> + <string name="cordn_coordinators_relays">%1$s</string> + <string name="cordn_coordinators_relays_label">Svarar på</string> + <string name="cordn_coordinators_relays_more">%1$s +%2$d till</string> + <string name="cordn_coordinators_remove">Ta bort</string> + <string name="cordn_coordinators_rename">Byt namn</string> + <string name="cordn_coordinators_server">Säger sig vara: %1$s</string> + <string name="cordn_coordinators_server_claim">En koordinators namn och version är strängar den själv valt. Dess publika nyckel är det enda som identifierar den.</string> + <string name="cordn_coordinators_server_silent">Svarade, men angav inget namn.</string> + <string name="cordn_coordinators_show_all">Visa alla %1$d</string> + <string name="cordn_coordinators_show_fewer">Visa färre</string> + <plurals name="cordn_coordinators_show_older"> + <item quantity="one">Visa %1$d som slutat annonsera</item> + <item quantity="other">Visa %1$d som slutat annonsera</item> + </plurals> + <string name="cordn_coordinators_stale_note">Dessa har inte annonserat på över en månad. Att skapa en grupp på en som försvunnit kommer att misslyckas.</string> + <string name="cordn_create_action">Skapa grupp</string> + <plurals name="cordn_create_action_invite"> + <item quantity="one">Skapa och bjud in %1$d person</item> + <item quantity="other">Skapa och bjud in %1$d personer</item> + </plurals> + <string name="cordn_create_add_member">Lägg till någon</string> + <string name="cordn_create_admin">Admin</string> + <plurals name="cordn_create_admin_count"> + <item quantity="one">%1$d person kan lägga till och ta bort</item> + <item quantity="other">%1$d personer kan lägga till och ta bort</item> + </plurals> + <string name="cordn_create_admin_only_me">Bara jag hanterar den här gruppen</string> + <string name="cordn_create_admin_only_me_on">Bara du kan bjuda in, ta bort medlemmar eller ändra gruppens uppgifter. Du kan lämna över det till någon annan senare.</string> + <string name="cordn_create_coordinator">Koordinator</string> + <string name="cordn_create_coordinator_change">Ändra</string> + <string name="cordn_create_coordinator_discover">Hitta koordinatorer</string> + <string name="cordn_create_coordinator_hide">Klar</string> + <string name="cordn_create_coordinator_new">En annan koordinator</string> + <string name="cordn_create_coordinator_pubkey">Koordinatorns publika nyckel (hex eller npub)</string> + <string name="cordn_create_coordinator_relays">Reläer den svarar på, ett per rad</string> + <plurals name="cordn_create_coverage_all"> + <item quantity="one">Når %1$d person — alla du valt</item> + <item quantity="other">Når alla dina %1$d personer</item> + </plurals> + <string name="cordn_create_coverage_partial">Når %1$d av %2$d — resten skulle bli utanför</string> + <string name="cordn_create_coverage_unknown">Inte kontrollerad — den tillfrågas om du väljer den</string> + <string name="cordn_create_description">Beskrivning (valfritt)</string> + <string name="cordn_create_egalitarian">Alla kan lägga till och ta bort</string> + <string name="cordn_create_egalitarian_note">Alla i gruppen kan bjuda in, ta bort medlemmar och ändra gruppens uppgifter. Vilken medlem som helst kan utse admins senare, vilket upphäver detta.</string> + <string name="cordn_create_explainer">En cordn-grupp ordnas av en koordinator du väljer. Gruppen skapas på den här enheten — koordinatorn får veta att gruppen finns först när du bjuder in någon eller skickar ett meddelande.</string> + <string name="cordn_create_failed">Gruppen kunde inte skapas.</string> + <string name="cordn_create_name">Gruppnamn</string> + <string name="cordn_create_no_coordinator">Ingen koordinator vald</string> + <string name="cordn_create_outcome_explainer">Varje inbjudan är ett Welcome-meddelande som lämnas hos koordinatorn, så de kan misslyckas en i taget. Ingen har meddelats: de ser det nästa gång de öppnar sin app.</string> + <string name="cordn_create_outcome_failed">Koordinatorn avvisade inbjudan</string> + <string name="cordn_create_outcome_link">Skicka en länk</string> + <string name="cordn_create_outcome_no_welcome">Tillagd i gruppen, men inbjudan nådde inte fram — försök igen från gruppskärmen</string> + <string name="cordn_create_outcome_open">Öppna chatten</string> + <string name="cordn_create_outcome_skipped">Ingen nyckel här, så inget Welcome-meddelande kunde lämnas</string> + <string name="cordn_create_outcome_title">Gruppen är skapad</string> + <string name="cordn_create_outcome_waiting">Väntar på dem</string> + <string name="cordn_create_people_done">Klar</string> + <string name="cordn_create_people_empty">Ingen tillagd ännu. En grupp kan skapas tom och inbjudningar skickas senare, men att ange personer nu är det som visar vilken koordinator som kan nå dem.</string> + <string name="cordn_create_people_none">Ingen ännu — tryck för att lägga till personer</string> + <string name="cordn_create_people_note">Att ange personer nu är det som talar om för oss vilken koordinator som kan nå dem. Var och en får ett Welcome-meddelande som lämnas hos den koordinatorn, och som de hämtar nästa gång de öppnar sin app.</string> + <plurals name="cordn_create_reach_count"> + <item quantity="one">Nåbar via %1$d koordinator</item> + <item quantity="other">Nåbar via %1$d koordinatorer</item> + </plurals> + <string name="cordn_create_reach_none">Ingen nyckel hos någon koordinator du använder</string> + <string name="cordn_create_reach_unknown">Inte kontrollerad ännu</string> + <string name="cordn_create_step_admins">Vem som kan lägga till och ta bort</string> + <string name="cordn_create_step_name">Namnge den</string> + <string name="cordn_create_step_people">Vilka som är med</string> + <string name="cordn_create_step_where">Var den finns</string> + <string name="cordn_create_title">Ny Cordn-grupp</string> + <plurals name="cordn_create_unreachable_count"> + <item quantity="one">%1$d person kan inte nås av någon koordinator du använder</item> + <item quantity="other">%1$d personer kan inte nås av någon koordinator du använder</item> + </plurals> + <string name="cordn_create_you">Du, alltid admin</string> + <string name="cordn_delete_confirm_body">Alla i gruppen kommer att se meddelandet som återkallat, och Amethyst slutar visa texten. Koordinatorn har fortfarande den krypterade kopia som skickades, och den som redan har läst det har redan läst det.</string> + <string name="cordn_delete_confirm_title">Återkalla meddelande</string> + <string name="cordn_delivery_accepted">Accepterat av koordinatorn</string> + <string name="cordn_exposure_close">Uppfattat</string> + <string name="cordn_exposure_open">Vad den här koordinatorn kan se</string> + <string name="cordn_group_info">Gruppinfo</string> + <string name="cordn_group_unavailable">Den här gruppen är inte tillgänglig. Dess koordinator kan ha tagits bort.</string> + <string name="cordn_group_unavailable_detail">Lägg till en koordinator i Inställningar för att börja använda cordn-grupper.</string> + <plurals name="cordn_groups_count"> + <item quantity="one">%1$d grupp</item> + <item quantity="other">%1$d grupper</item> + </plurals> + <string name="cordn_groups_down_note">Inget kommer fram från de här grupperna. Amethyst fortsätter att försöka.</string> + <string name="cordn_groups_manage">Koordinatorer</string> + <string name="cordn_groups_none">Inga cordn-grupper ännu</string> + <string name="cordn_groups_none_detail">En cordn-grupp finns hos en koordinator som håller reda på medlemskapet och skickar meddelanden vidare, utan att någonsin se vad någon skriver.</string> + <string name="cordn_groups_start">Starta en</string> + <string name="cordn_info_add_member">Lägg till någon</string> + <string name="cordn_info_add_member_none">Ingen hittades med det namnet.</string> + <string name="cordn_info_add_member_note">De kan bara läggas till om de har publicerat ett nyckelpaket till den här koordinatorn.</string> + <string name="cordn_info_add_member_placeholder">Namn, npub eller namn@domän</string> + <string name="cordn_info_admin_badge">Admin</string> + <plurals name="cordn_info_admins_kept"> + <item quantity="one">Gruppens %1$d admin förblir oförändrad.</item> + <item quantity="other">Gruppens %1$d admins förblir oförändrade.</item> + </plurals> + <string name="cordn_info_coordinator">Koordinator</string> + <string name="cordn_info_coordinator_key">Koordinatornyckel</string> + <string name="cordn_info_coordinator_nprofile">Koordinatorlänk</string> + <string name="cordn_info_copy_nprofile">Kopiera koordinatorns nprofile</string> + <string name="cordn_info_edit_details">Redigera uppgifter</string> + <string name="cordn_info_egalitarian">Den här gruppen har inga admins: alla kan lägga till, ta bort och byta namn, permanent.</string> + <string name="cordn_info_epoch">Epok</string> + <string name="cordn_info_gid">Grupp-id</string> + <string name="cordn_info_has_key_package">Kan läggas till via den här koordinatorn</string> + <string name="cordn_info_members">Medlemmar</string> + <string name="cordn_info_remove_confirm_body">%1$s slutar ta emot meddelanden härifrån. De behåller det de redan har läst, och detta kan inte ångras — de skulle behöva bjudas in igen.</string> + <string name="cordn_info_remove_confirm_title">Ta bort från gruppen?</string> + <string name="cordn_info_remove_member">Ta bort</string> + <string name="cordn_info_save">Spara</string> + <string name="cordn_info_technical">Tekniska detaljer</string> + <string name="cordn_invitations_accept">Gå med</string> + <string name="cordn_invitations_decline">Avböj</string> + <string name="cordn_invitations_decline_warning">Att avböja är permanent. För att komma in igen måste du bjudas in på nytt.</string> + <string name="cordn_invitations_explainer">Kontrolleras när du öppnar den här skärmen, och aldrig annars — att fråga en koordinator om någon har bjudit in dig avslöjar att du finns här, så Amethyst gör det inte i bakgrunden.</string> + <string name="cordn_invitations_load_failed">Kunde inte läsa inbjudningar.</string> + <plurals name="cordn_invitations_members"> + <item quantity="one">%1$d person är redan med i den här gruppen</item> + <item quantity="other">%1$d personer är redan med i den här gruppen</item> + </plurals> + <string name="cordn_invitations_members_more">+%1$d till</string> + <string name="cordn_invitations_no_coordinators">Du har ingen koordinator öppen. cordn-inbjudningar kommer via en koordinator, så det finns ingenstans att leta ännu.</string> + <string name="cordn_invitations_none">Inga väntande inbjudningar.</string> + <string name="cordn_invitations_refresh">Kontrollera igen</string> + <string name="cordn_invitations_skipped">En inbjudan lämnades till en annan enhet</string> + <string name="cordn_invitations_title">Cordn-inbjudningar</string> + <string name="cordn_invitations_unreachable">%1$s svarade inte</string> + <string name="cordn_invitations_via">Via %1$s</string> + <string name="cordn_keypackages_disclosure">Publicering signerar ett belägg med din egen kontonyckel. Den koordinatorn vet då att kontot finns och går att bjuda in — permanent, och oavsett om någon bjuder in dig.</string> + <string name="cordn_keypackages_explainer">Andra kan bara lägga till dig i en cordn-grupp genom att ta ett nyckelpaket du publicerat till den koordinatorn. cordn har ingen annan plats att förvara ett, så utan något publicerat kan ingen bjuda in dig — och inget talar om för dem varför.</string> + <string name="cordn_keypackages_failed">Kunde inte läsa nyckelpaketen.</string> + <string name="cordn_keypackages_last_resort_no">Inget reservpaket. När engångspaketen tar slut misslyckas inbjudningar.</string> + <string name="cordn_keypackages_last_resort_yes">Ett reservpaket är publicerat, så en inbjudan kan fortfarande göras när engångspaketen tagit slut.</string> + <plurals name="cordn_keypackages_orphans"> + <item quantity="one">%1$d paket som den här enheten inte kan öppna</item> + <item quantity="other">%1$d paket som den här enheten inte kan öppna</item> + </plurals> + <string name="cordn_keypackages_orphans_body">Koordinatorn lämnar ut dessa till den som bjuder in dig, men den privata halvan finns inte på den här enheten — den tillhör en annan installation. Om den installationen är borta ger en inbjudan med något av dessa ett Welcome-meddelande som ingen någonsin kan öppna.</string> + <string name="cordn_keypackages_publish">Publicera ett</string> + <string name="cordn_keypackages_publish_last_resort">Publicera reservpaket</string> + <plurals name="cordn_keypackages_summary"> + <item quantity="one">%1$d engångspaket tillgängligt.</item> + <item quantity="other">%1$d engångspaket tillgängliga.</item> + </plurals> + <string name="cordn_keypackages_topup_note">När du väl har publicerat här ersätter Amethyst tyst paketen allteftersom de används. Det första publicerar den aldrig åt dig.</string> + <string name="cordn_keypackages_withdraw_all">Återkalla alla</string> + <string name="cordn_keypackages_withdraw_orphans">Återkalla dem</string> + <string name="cordn_link_request">Be att få gå med i gruppen</string> + <string name="cordn_link_request_disclosure">Detta publicerar ett nyckelpaket med din egen kontonyckel och talar om för koordinatorn att du vill gå med i gruppen — oavsett om någon svarar.</string> + <string name="cordn_link_request_failed">Kunde inte be att få gå med.</string> + <string name="cordn_link_request_sent">Förfrågan skickad. En medlem i gruppen måste lägga till dig; inbjudan dyker upp under cordn-inbjudningar.</string> + <string name="cordn_link_scan">Skanna</string> + <string name="cordn_media_download_failed">Kunde inte öppna den filen.</string> + <string name="cordn_media_no_server">Ingen mediaserver är inställd för det här kontot, så filen har ingenstans att ta vägen. Välj en i Inställningar.</string> + <string name="cordn_media_unreadable">Filen kunde inte läsas.</string> + <string name="cordn_media_upload_failed">Kunde inte skicka den filen.</string> + <plurals name="cordn_member_count"> + <item quantity="one">%1$d medlem</item> + <item quantity="other">%1$d medlemmar</item> + </plurals> + <string name="cordn_message_deleted">Meddelandet raderat</string> + <string name="cordn_message_details_coordinator">Koordinator</string> + <string name="cordn_message_details_cursor">Markör</string> + <string name="cordn_message_details_sent_at">Skickat</string> + <string name="cordn_message_details_title">Meddelandedetaljer</string> + <string name="cordn_message_edited">redigerat</string> + <string name="cordn_pinned_by">Fäst av %1$s</string> + <plurals name="cordn_pinned_count"> + <item quantity="one">%1$d fäst meddelande i den här gruppen</item> + <item quantity="other">%1$d fästa meddelanden i den här gruppen</item> + </plurals> + <string name="cordn_pinned_next">Nästa fästa meddelande</string> + <string name="cordn_pinned_previous">Föregående fästa meddelande</string> + <string name="cordn_pinned_show_all">Visa alla fästa meddelanden</string> + <string name="cordn_pinned_title">Fästa meddelanden</string> + <string name="cordn_reactions_title">Reaktioner</string> + <string name="cordn_requests_accept">Lägg till</string> + <string name="cordn_requests_admin_only">Bara en admin kan godkänna. I en grupp utan admins är det alla.</string> + <string name="cordn_requests_check">Sök efter förfrågningar</string> + <string name="cordn_requests_decline">Avfärda</string> + <string name="cordn_requests_failed">Kunde inte läsa förfrågningarna.</string> + <string name="cordn_requests_none">Ingen väntar på att få gå med.</string> + <string name="cordn_requests_title">Förfrågningar om att gå med</string> + <string name="cordn_send">Skicka</string> + <string name="cordn_send_failed">Det gick inte att skicka.</string> + <string name="cordn_send_no_session">Den här gruppens koordinator är inte öppen, så inget kan skickas ännu.</string> + <string name="cordn_share_copied">Kopierad</string> + <string name="cordn_share_copy">Kopiera länk</string> + <string name="cordn_share_explainer">Alla som har den här länken kan be att få gå med. En admin måste fortfarande släppa in dem.</string> + <string name="cordn_share_title">Dela den här gruppen</string> + <string name="cordn_voice_play">Spela upp röstmeddelande</string> + <string name="cordn_voice_record">Spela in ett röstmeddelande</string> + <string name="cordn_voice_stop">Stoppa och skicka</string> + <string name="qr_scanner_camera_rationale">Amethyst behöver kameran för att skanna QR-koder. Inget spelas in eller laddas upp — bilderna avkodas på enheten och kastas sedan.</string> + <string name="qr_scanner_grant_camera">Tillåt kamera</string> + <string name="qr_scanner_camera_blocked">Kameraåtkomst är avstängd för Amethyst. Du kan slå på den igen i Androids inställningar.</string> + <string name="qr_scanner_open_settings">Öppna inställningar</string> + <string name="qr_scanner_unavailable">QR-avkodaren kunde inte starta på den här enheten. Du kan fortfarande klistra in koden som text.</string> + <string name="qr_scanner_torch_on">Tänd lampan</string> + <string name="qr_scanner_torch_off">Släck lampan</string> + <string name="qr_scanner_dark_hint">Det är mörkt — prova lampan</string> + <string name="qr_scanner_scan_image">Skanna en bild</string> + <string name="qr_scanner_paste">Klistra in</string> + <string name="qr_scanner_no_code_in_image">Ingen QR-kod hittades i bilden</string> + <string name="qr_scanner_clipboard_empty">Det finns inget i urklipp att skanna</string> + <string name="qr_scanner_pick_one">Tryck på koden du vill ha</string> + <plurals name="qr_scanner_sequence_progress"> + <item quantity="one">Fångade %1$s av %2$s del — fortsätt</item> + <item quantity="other">Fångade %1$s av %2$s delar — fortsätt</item> + </plurals> + <string name="qr_scanner_zoom_reset">Återställ zoom</string> + <string name="qr_scanner_unsupported_title">Amethyst kan inte öppna den här koden</string> + <string name="qr_scanner_unsupported_secret">Den här koden innehåller en privat nyckel eller en parningshemlighet, så innehållet visas inte här. Använd skärmen som förväntar sig den.</string> + <string name="qr_scanner_kind_nsec">Det här är en privat nyckel. Klistra in den på inloggningsskärmen i stället.</string> + <string name="qr_scanner_kind_wallet">Det här är en plånboksanslutning. Lägg till den under Plånboksinställningar.</string> + <string name="qr_scanner_kind_bunker">Det här är en fjärrsignatäradress. Amethyst delar ut sådana så att andra appar kan be den signera — den kan inte logga in med en.</string> + <string name="qr_scanner_kind_signer">Det här är en app som vill ansluta till din signatär. Öppna den från NIP-46-signatärskärmen.</string> + <string name="qr_scanner_kind_lightning">Det här är en Lightning-faktura.</string> + <string name="qr_scanner_kind_cashu">Det här är en Cashu-token.</string> + <string name="qr_scanner_kind_web">Det här är en webblänk.</string> + <string name="qr_scanner_kind_text">Det här är vanlig text, inte en Nostr-kod.</string> + <string name="qr_scanner_kind_nostr_unsupported">Det här är en Nostr-kod, men inte en som den här skärmen kan öppna.</string> + <string name="qr_scanner_label_profile">Profil</string> + <string name="qr_scanner_label_note">Anteckning</string> + <string name="qr_scanner_label_nostr">Nostr-kod</string> + <string name="qr_scanner_label_relay">Relä</string> + <string name="qr_scanner_label_nsec">Privat nyckel</string> + <string name="qr_scanner_label_wallet">Plånboksanslutning</string> + <string name="qr_scanner_label_bunker">Fjärrsignatäradress</string> + <string name="qr_scanner_label_signer">Anslutningsbegäran till signatär</string> + <string name="qr_scanner_label_lightning">Lightning-faktura</string> + <string name="qr_scanner_label_cashu">Cashu-token</string> + <string name="qr_scanner_label_web">Webblänk</string> + <string name="qr_scanner_label_text">Vanlig text</string> + <string name="qr_scanner_open_link">Öppna länk</string> + <string name="qr_scanner_copy">Kopiera</string> + <string name="qr_scanner_copied">Kopierat</string> + <string name="qr_scanner_try_again">Skanna igen</string> + <string name="workout_suggestion_connect_title">Använd träningspassen du redan har registrerat</string> + <string name="workout_suggestion_connect_message">Anslut Health Connect (Samsung Health, Google Fit, Fitbit, Garmin…) för att bygga Min träning, din egen träningssammanfattning, och för att fylla i det här passet från ett som din klocka redan har registrerat i stället för att skriva in varje siffra för hand.</string> + <string name="health_connect_rationale_headline">Amethyst använder Health Connect för att bygga Min träning — din egen träningssammanfattning, på din telefon.</string> + <string name="health_connect_rationale_intro">Min träning (meny → Du → Min träning) visar hur du har tränat: hur mycket du gjorde den här veckan jämfört med förra, hur din tid fördelas mellan löpning, cykling, promenader, simning och gym, dina bästa prestationer, hur många dagar du tränade och din nuvarande svit. Amethyst bygger den bilden från träningspassen som din klocka eller träningsapp redan har sparat i Health Connect, tillsammans med pass du har loggat i Amethyst. Allt räknas ut på din telefon och visas för dig.</string> + <string name="health_connect_rationale_exercise">Träning · vilken aktivitet du utförde, när den började och hur länge den varade. Det här är själva passet: det styr antalet pass, din träningstid, uppdelningen per aktivitet, dina aktiva dagar och din svit.</string> + <string name="health_connect_rationale_distance">Distans · hur långt du tog dig, visat som din distans den här veckan jämfört med förra, ditt veckosnitt, din distans per aktivitet och din längsta distans.</string> + <string name="health_connect_rationale_calories">Aktiva och totala kalorier · energin ett pass förbrände, visat som din energi den här veckan och hur den står sig mot förra veckan. Aktiva kalorier används när din källa registrerar dem; totala kalorier används som reserv för källor som bara registrerar total energi.</string> + <string name="health_connect_rationale_heart_rate">Puls · genomsnitt och maxvärde för varje pass, sammanslaget till din genomsnittliga och högsta puls för perioden. Det är så du skiljer en lätt vecka från en tuff med samma distans.</string> + <string name="health_connect_rationale_steps">Steg · antalet steg under en löptur, promenad eller vandring, visat som ditt veckosnitt för steg och ditt högsta antal steg.</string> + <string name="health_connect_rationale_elevation">Höjdmeter · hur mycket du klättrade, visat som ditt veckosnitt för stigning och din största stigning. Det är det som skiljer en platt vecka från en kuperad.</string> + <string name="health_connect_rationale_limit_window">Läser bara träningspass från de senaste 4 veckorna, och bara medan Min träning eller passredigeraren är öppen. Läser aldrig i bakgrunden.</string> + <string name="health_connect_rationale_limit_publish">Skickar aldrig dina hälsodata någonstans. Min träning räknas ut på din telefon och stannar där. Om du senare vill publicera ett pass trycker du på Dela för det passet, läser inlägget som Amethyst fyllt i och publicerar det själv — ett pass i taget, aldrig automatiskt.</string> + <string name="health_connect_rationale_limit_optional">Hela funktionen är valfri. Återkalla behörigheterna i Health Connect när som helst — Min träning går då tillbaka till sin uppmaning — eller stäng av förslagen i redigeraren under Inställningar → Skrivinställningar. Resten av Amethyst fortsätter att fungera.</string> + <string name="my_fitness_title">Min träning</string> + <string name="my_fitness_window">Senaste 4 veckorna</string> + <string name="my_fitness_loading_metrics">Läser in distans, kalorier och puls…</string> + <string name="my_fitness_window_note">De senaste fyra hela veckorna av din träning: passen du har publicerat, plus allt Health Connect registrerar på den här telefonen. Amethyst räknar ut sammanfattningen på enheten och publicerar ingenting av den.</string> + <string name="my_fitness_this_week">Den här veckan</string> + <string name="my_fitness_vs_last_week">jämfört med förra veckan</string> + <string name="my_fitness_weekly_average">Veckosnitt</string> + <string name="my_fitness_workouts">Pass</string> + <string name="my_fitness_active_days">Aktiva dagar</string> + <string name="my_fitness_streak">Dagar i rad</string> + <string name="my_fitness_time">Tid</string> + <string name="my_fitness_duration_hours_minutes">%1$d h %2$d min</string> + <string name="my_fitness_duration_minutes">%1$d min</string> + <string name="my_fitness_duration_seconds">%1$d s</string> + <string name="my_fitness_distance">Distans</string> + <string name="my_fitness_calories">Kalorier</string> + <string name="my_fitness_steps">Steg</string> + <string name="my_fitness_elevation">Stigning</string> + <string name="my_fitness_avg_heart_rate">Snittpuls</string> + <string name="my_fitness_max_heart_rate">Maxpuls</string> + <string name="my_fitness_by_activity">Per aktivitet</string> + <string name="my_fitness_bests">Bästa prestationer</string> + <string name="my_fitness_best_longest_distance">Längsta distans</string> + <string name="my_fitness_best_longest_duration">Längsta pass</string> + <string name="my_fitness_best_biggest_climb">Största stigning</string> + <string name="my_fitness_best_most_steps">Flest steg</string> + <string name="my_fitness_best_highest_heart_rate">Högsta puls</string> + <string name="my_fitness_recent">Senaste passen</string> + <string name="my_fitness_empty">Inget loggat de senaste 4 veckorna. Pass du publicerar, och de som din klocka registrerar, visas här.</string> + <string name="my_fitness_connect_title">Följ din träning</string> + <string name="my_fitness_connect_message">Anslut Health Connect för att se dina veckosummor, utvecklingen vecka för vecka, bästa prestationer och träningssvit. Allt stannar på din telefon.</string> + <string name="my_fitness_connect_banner">Anslut Health Connect för att lägga till passen din klocka registrerar, med puls, steg och stigning.</string> + <string name="my_fitness_connect_button">Anslut</string> + <string name="my_fitness_share">Dela det här passet</string> + <string name="my_fitness_unit_bpm">bpm</string> + <string name="my_fitness_unit_kcal">kcal</string> + <string name="my_fitness_unit_km">km</string> + <string name="my_fitness_unit_mi">mi</string> + <string name="my_fitness_unit_m">m</string> + <string name="my_fitness_unit_ft">ft</string> + <string name="looking_for_event_title">Läser in</string> + <string name="looking_for_private_event">Väntar på att dina privata meddelanden ska läsas in…</string> + <string name="call_duration_minutes_seconds">%1$02d:%2$02d</string> + <string name="call_duration_hours_minutes_seconds">%1$d:%2$02d:%3$02d</string> + <string name="marmot_preview_group_updated">Gruppen uppdaterad</string> + <string name="marmot_preview_media">Bilaga</string> + <string name="marmot_preview_no_text">Meddelande</string> + <string name="marmot_preview_with_sender">%1$s: %2$s</string> + <string name="marmot_empty_create_action">Skapa en grupp</string> + <string name="marmot_invite_device_banner">Marmot-inbjudningar går till en annan enhet och kan inte öppnas här.</string> + <string name="marmot_invite_device_banner_dismiss">Avfärda</string> + <string name="marmot_invite_device_publish">Publicera från den här enheten</string> + <string name="wallet_open_zapped_note">Öppna zappad anteckning</string> + <string name="video_player_settings_action_captions">Undertexter</string> + <string name="video_player_settings_action_captions_description">Slå på eller av undertexter (dolt när videon saknar sådana)</string> + <string name="app_notification_reply_failed">Inte skickat</string> + <string name="app_notification_reply_retry">Försök igen</string> + <string name="app_notification_reply_sending">Skickar…</string> + <string name="app_notification_reply_unconfirmed">Skickar fortfarande…</string> + <string name="app_notification_shortcut_account_removed">Det här kontot har tagits bort från Amethyst.</string> + <string name="captions_off">Av</string> + <string name="captions_turn_off">Stäng av undertexter</string> + <string name="captions_turn_on">Slå på undertexter</string> + <string name="captions_unnamed_track">Undertexter</string> + <string name="kind_app_curation_set">Applista</string> + <string name="kind_article_curation_set">Artikellista</string> + <string name="kind_bookmark_set">Bokmärkessamling</string> + <string name="kind_favorite_algo_feeds">Favoritflöden</string> + <string name="kind_git_authors">Git-författare</string> + <string name="kind_git_repositories">Git-förråd</string> + <string name="kind_good_wiki_authors">Betrodda wikiförfattare</string> + <string name="kind_good_wiki_relays">Betrodda wikireläer</string> + <string name="kind_interest_set">Intressen</string> + <string name="kind_media_follows">Mediaföljningar</string> + <string name="kind_media_starter_pack">Startpaket för media</string> + <string name="kind_mute_set_for_kind">Tystat för typ %1$s</string> + <string name="kind_picture_curation_set">Bildlista</string> + <string name="kind_simple_groups">Grupper</string> + <string name="kind_video_collaboration">Videosamarbete</string> + <string name="kind_video_list">Videolista</string> + <string name="kind_video_subtitles">Videoundertexter</string> + <string name="login_bunker_not_supported">Amethyst för Android kan inte logga in med en bunker://-adress. Den delar ut sådana så att andra appar kan be den signera, men den kan inte logga in med en. Använd en extern signatärapp eller Amethyst Desktop.</string> + <string name="login_nostrconnect_not_supported">Det där är en app som vill ansluta till din signatär, inte ett sätt att logga in. Logga in först och öppna den sedan från NIP-46-signatärskärmen.</string> + <string name="my_fitness">Min träning</string> + <string name="nip51_list_has_private_members">Den här listan har privata medlemmar som bara dess skapare kan se</string> + <string name="route_edit_geocache">Redigera cache</string> + <string name="route_edit_geocache_hunt">Redigera jakt</string> + <string name="route_geocache_detail">Cache</string> + <string name="route_geocache_hunt_detail">Jakt</string> + <string name="route_geocache_hunts">Cachejakter</string> + <string name="route_geocaches">Geocacher</string> + <string name="route_log_geocache_find">Logga ett fynd</string> + <string name="route_new_geocache">Göm en cache</string> + <string name="route_new_geocache_hunt">Ny jakt</string> + <string name="video_collaboration_accepted_on">Accepterade ett samarbetsomnämnande</string> + <string name="video_collaboration_declined_on">Avböjde ett samarbetsomnämnande</string> + <string name="video_credit_accepted">Omnämnande accepterat</string> + <string name="video_credit_audio_from">Ljud från</string> + <string name="video_credit_featuring">Med</string> + <string name="video_credit_inspired_by">Inspirerad av</string> + <string name="video_credit_references">Referenser</string> + <string name="video_curation_set_private_items">Privat lista</string> + <plurals name="video_curation_set_video_count"> + <item quantity="one">%1$d video</item> + <item quantity="other">%1$d videor</item> + </plurals> + <string name="marmot_invite_device_success">Den här enheten tar nu emot dina Marmot-inbjudningar.</string> + <string name="marmot_invite_device_failure">Det gick inte att publicera KeyPackage: %1$s</string> + <string name="marmot_invite_device_rejected">Inget relä accepterade KeyPackage. Kontrollera dina reläinställningar och försök igen.</string> + <string name="chat_delivery_sending">Skickar…</string> + <string name="chat_delivery_failed">Kunde inte skickas. Tryck för att försöka igen.</string> + <plurals name="marmot_unread_messages"> + <item quantity="one">%1$d nytt meddelande</item> + <item quantity="other">%1$d nya meddelanden</item> + </plurals> + <string name="chat_type_cordn_title">Cordn-grupper</string> + <string name="chat_type_cordn_desc">End-to-end-krypterade grupper som ordnas av en koordinator du väljer, i stället för av reläer.</string> + <string name="new_conversation_cordn_title">Cordn-grupp</string> + <string name="new_conversation_cordn_tagline">Krypterad gruppchatt som ordnas av en koordinator du väljer.</string> + <string name="new_conversation_cordn_chip">Koordinerad</string> + <string name="new_conversation_cordn_best">Bäst för team som vill ha en pålitlig ordning i konversationen.</string> + <string name="new_conversation_cordn_cta">Skapa Cordn-grupp</string> + <string name="new_conversation_cordn_pro_1">End-to-end-krypterad med MLS — koordinatorn kan aldrig läsa ett meddelande.</string> + <string name="new_conversation_cordn_pro_2">En gemensam ordning för alla, så att historiken inte kan kastas om av en felaktig klocka.</string> + <string name="new_conversation_cordn_con_1">Koordinatorn får veta vilka som är med i gruppen och när ni pratar, även om den inte kan läsa vad ni säger.</string> + <string name="sno_object_title">3D-objekt</string> + <string name="sno_object_details">%1$d hörn · %2$d ytor</string> + <string name="sno_object_unreadable">Det här 3D-objektet kunde inte läsas (regel %1$s)</string> + <string name="sno_avatar_title">Cyberspace-avatar</string> + <string name="sno_avatar_unpaid">Den här avataren har inte betalat för sin storlek, så den ritas inte</string> + <string name="sno_light_on">Belyst; tryck för att rita objektet platt</string> + <string name="sno_light_off">Platt; tryck för att belysa objektet</string> + <string name="sno_object_scale">1 enhet = %1$s</string> + <string name="sno_shard_dataspace">Gömd i dataspace · %1$s</string> + <string name="sno_shard_ideaspace">Gömd i ideaspace · %1$s</string> + <string name="sno_avatar_default">Standardavatar</string> + <string name="sno_avatar_default_details">Ingen form publicerad, så alla ser den här</string> + <string name="cyberspace_bag_title">Gömd på en plats</string> + <string name="cyberspace_bag_box">Gömd i en låda med %1$s regioner</string> + <string name="cyberspace_bag_destination">Gömd i en enda region, som ledtråden anger exakt</string> + <string name="cyberspace_bag_no_hint">Ingen ledtråd, så den kan vara var som helst i cyberspace</string> + <string name="cyberspace_bag_out_of_reach">Utom räckhåll: ledtråden anger 2^%1$d regioner att söka i</string> + <string name="cyberspace_bag_search">Sök efter den</string> + <string name="cyberspace_bag_cancel">Stoppa</string> + <string name="cyberspace_bag_measuring">Beräknar sökningens kostnad på den här enheten…</string> + <string name="cyberspace_bag_progress">Sökt i %1$s av %2$s regioner</string> + <plurals name="cyberspace_bag_too_slow_minutes"> + <item quantity="one">Utom räckhåll: cirka %1$d minuts sökning på den här enheten</item> + <item quantity="other">Utom räckhåll: cirka %1$d minuters sökning på den här enheten</item> + </plurals> + <plurals name="cyberspace_bag_too_slow_hours"> + <item quantity="one">Utom räckhåll: cirka %1$d timmes sökning på den här enheten</item> + <item quantity="other">Utom räckhåll: cirka %1$d timmars sökning på den här enheten</item> + </plurals> + <string name="cyberspace_bag_not_found">Inte i lådan som ledtråden angav</string> + <string name="cyberspace_bag_damaged">Regionen hittades, men innehållet kunde inte läsas</string> + <plurals name="cyberspace_bag_dropped"> + <item quantity="one">%1$d föremål som gömts här matchade inte sin signatur och kastades</item> + <item quantity="other">%1$d föremål som gömts här matchade inte sina signaturer och kastades</item> + </plurals> + <string name="cyberspace_bag_empty">Öppnad, och det fanns ingenting i den</string> + <string name="cyberspace_bag_unsigned">Osignerad, så den här författaren är bara ett påstående</string> + <string name="cyberspace_bag_unknown_kind">Ett föremål av typ %1$d, som den här klienten inte ritar</string> + <plurals name="cyberspace_bag_opaque"> + <item quantity="one">%1$d byte av något som den här klienten inte läser</item> + <item quantity="other">%1$d byte av något som den här klienten inte läser</item> + </plurals> + <string name="browser_pill_close">Stäng</string> + <string name="browser_pill_tile_copy">Kopiera länk</string> + <string name="browser_pill_tile_find">Sök</string> + <string name="browser_pill_tile_text">Textstorlek</string> + <string name="browser_pill_tile_desktop">Datorversion</string> + <string name="browser_pill_tile_home">Till startskärm</string> + <string name="browser_pill_tile_other">Annan webbläsare</string> + <string name="browser_pill_tile_full">Helskärm</string> + <string name="browser_pill_edit_address">Redigera adress</string> + <string name="browser_pill_back">Bakåt</string> + <string name="browser_pill_forward">Framåt</string> + <string name="browser_pill_reload">Ladda om</string> + <string name="browser_pill_stop">Sluta läsa in</string> + <string name="browser_pill_favorite_add">Lägg till i favoriter</string> + <string name="browser_pill_favorite_remove">Ta bort från favoriter</string> + <string name="browser_pill_share">Dela</string> + <string name="browser_pill_copy">Kopiera länk</string> + <string name="browser_pill_find">Sök på sidan</string> + <string name="browser_pill_text_size">Textstorlek</string> + <string name="browser_pill_add_home">Lägg till på startskärmen</string> + <string name="browser_pill_desktop">Datorversion av webbplatsen</string> + <string name="browser_pill_other_browser">Öppna i webbläsare</string> + <string name="browser_pill_other_browser_named">Öppna i %1$s</string> + <string name="browser_pill_full_screen">Helskärm</string> + <string name="browser_pill_left_site">Du lämnade %1$s</string> + <string name="browser_pill_back_to_app">Tillbaka till appen</string> + <string name="browser_pill_text_smaller">Mindre text</string> + <string name="browser_pill_text_larger">Större text</string> + <string name="browser_pill_text_reset">Återställ</string> + <string name="browser_pill_text_value">%1$d%</string> + <string name="browser_pill_privacy">Integritet</string> + <string name="browser_pill_tor_title">Onion-routing</string> + <string name="browser_pill_tor_on">Webbplatsen kan inte se din IP-adress</string> + <string name="browser_pill_tor_off">Webbplatsen kan se din IP-adress</string> + <string name="browser_pill_site_settings">Webbplatsinställningar</string> + <string name="browser_pill_site_settings_none">Inget tillåtet ännu</string> + <string name="browser_pill_access">Vad den kan komma åt</string> + <string name="browser_pill_access_desc">Dina nycklar lämnar aldrig Amethyst</string> + <string name="browser_pill_access_keys_desc">Varje signering, publicering, uppladdning eller betalning kräver ditt godkännande</string> + <string name="browser_pill_access_none">Ingen särskild åtkomst</string> + <string name="browser_pill_access_none_desc">Den körs i sandlåda och kan inte agera på ditt konto</string> + <string name="browser_pill_access_manage">Hantera behörigheter</string> + <string name="browser_pill_access_nsite">nSite · i sandlåda</string> + <string name="browser_pill_access_napplet">nApplet · i sandlåda</string> + <string name="browser_pill_console">Konsol</string> + <string name="browser_pill_security_https">Säker anslutning</string> + <string name="browser_pill_security_http">Inte säker</string> + <string name="browser_pill_security_tor">Onion-routad</string> + <string name="browser_pill_security_sandbox">App i sandlåda</string> + <string name="browser_pill_permission_camera">Kamera</string> + <string name="browser_pill_permission_microphone">Mikrofon</string> + <string name="browser_pill_permission_location">Plats</string> + <string name="browser_pill_decision_ask">Fråga</string> + <string name="browser_pill_decision_allow">Tillåt</string> + <string name="browser_pill_decision_block">Blockera</string> + <string name="browser_pill_permission_state">%1$s %2$s</string> + <string name="browser_pill_decision_allowed">tillåten</string> + <string name="browser_pill_decision_blocked">blockerad</string> + <string name="browser_pill_address_hint">Sök eller ange adress</string> + <string name="browser_pill_go">Gå</string> + <string name="browser_pill_clear">Rensa</string> + <string name="browser_pill_paste_go">Klistra in och gå</string> + <string name="browser_pill_fill_in">Använd den här adressen</string> + <string name="browser_pill_find_hint">Sök på sidan</string> + <string name="browser_pill_find_count">%1$d / %2$d</string> + <string name="browser_pill_find_none">Inga träffar</string> + <string name="browser_pill_find_previous">Föregående träff</string> + <string name="browser_pill_find_next">Nästa träff</string> + <string name="browser_pill_find_close">Stäng sökning på sidan</string> + <string name="browser_pill_console_all">Alla</string> + <string name="browser_pill_console_errors">Fel</string> + <string name="browser_pill_console_warnings">Varningar</string> + <string name="browser_pill_console_copy">Kopiera</string> + <string name="browser_pill_console_clear">Rensa</string> + <string name="browser_pill_console_empty">Inget loggat ännu</string> + <string name="browser_pill_perm_title">Den här webbplatsen vill använda</string> + <string name="browser_pill_perm_camera_desc">Se det din kamera ser</string> + <string name="browser_pill_perm_microphone_desc">Höra det din mikrofon hör</string> + <string name="browser_pill_perm_location_desc">Veta ungefär var du är</string> + <string name="browser_pill_perm_tor_note">Samtal kan avslöja din IP-adress även via Tor.</string> + <string name="browser_pill_perm_allow">Tillåt under besöket</string> + <string name="browser_pill_perm_once">Bara den här gången</string> + <string name="browser_pill_perm_deny">Tillåt inte</string> + <string name="browser_pill_dialog_says">%1$s säger</string> + <string name="browser_pill_dialog_generic">Den här sidan säger</string> + <string name="browser_pill_dialog_block">Låt inte den här sidan visa fler dialogrutor</string> + <string name="browser_pill_dialog_answer">Ditt svar</string> + <string name="browser_pill_dialog_leave_title">Lämna webbplatsen?</string> + <string name="browser_pill_dialog_leave_message">Ändringar du har gjort kanske inte sparas.</string> + <string name="browser_pill_dialog_leave">Lämna</string> + <string name="browser_pill_ok">OK</string> + <string name="browser_pill_cancel">Avbryt</string> + <string name="browser_pill_info_connection">Anslutning</string> + <string name="browser_pill_info_https">Anslutningen är krypterad</string> + <string name="browser_pill_info_https_desc">Information du skickar kan inte läsas på vägen.</string> + <string name="browser_pill_info_http">Anslutningen är inte krypterad</string> + <string name="browser_pill_info_http_desc">Ange inte lösenord eller betalningsuppgifter.</string> + <string name="browser_pill_info_tor">Onion-routad via Tor</string> + <string name="browser_pill_info_open">Öppen webb</string> + <string name="browser_pill_info_certificate">Certifikat</string> + <string name="browser_pill_info_certificate_desc">Utfärdat till %1$s av %2$s · giltigt till %3$s</string> + <string name="browser_pill_info_permissions">Behörigheter</string> + <string name="browser_pill_info_site_data">Cookies och webbplatsdata</string> + <string name="browser_pill_info_site_data_desc">Inloggningar och inställningar som den här webbplatsen sparat för det här kontot</string> + <string name="browser_pill_info_clear">Rensa webbplatsdata</string> + <string name="browser_pill_info_clear_confirm">Logga ut från den här webbplatsen och radera dess data?</string> + <string name="backup_action_block_again">Blockera %1$s igen</string> + <string name="backup_action_keep_count">Behåll %1$s</string> + <string name="backup_action_rejoin">Gå med i %1$s igen</string> + <string name="backup_action_remute">Tysta %1$s igen</string> + <string name="backup_action_restore_count">Återställ %1$s</string> + <string name="backup_action_restore_feeds">Återställ flöden</string> + <string name="backup_action_restore_places">Återställ platser</string> + <string name="backup_action_restore_profile">Återställ profil</string> + <string name="backup_action_restore_providers">Återställ leverantörer</string> + <string name="backup_action_restore_relays">Återställ reläer</string> + <string name="backup_action_restore_topics">Återställ ämnen</string> + <string name="backup_action_restore_wallet">Återställ plånbok</string> + <string name="backup_card_accept">Acceptera</string> + <plurals name="backup_card_and_added"> + <item quantity="one">%1$s och lade till %2$d</item> + <item quantity="other">%1$s och lade till %2$d</item> + </plurals> + <plurals name="backup_card_and_blocked"> + <item quantity="one">%1$s och blockerade %2$d</item> + <item quantity="other">%1$s och blockerade %2$d</item> + </plurals> + <plurals name="backup_card_and_followed"> + <item quantity="one">%1$s och följde %2$d</item> + <item quantity="other">%1$s och följde %2$d</item> + </plurals> + <plurals name="backup_card_and_joined"> + <item quantity="one">%1$s och gick med i %2$d</item> + <item quantity="other">%1$s och gick med i %2$d</item> + </plurals> + <plurals name="backup_card_and_muted"> + <item quantity="one">%1$s och tystade %2$d</item> + <item quantity="other">%1$s och tystade %2$d</item> + </plurals> + <string name="backup_card_app_settings">Dina synkade Amethyst-appinställningar ändrades i en annan app som inte är Amethyst. Det kan orsaka problem.</string> + <string name="backup_card_concord_emptied">Du tömde din Concord-lista</string> + <string name="backup_card_concord_rewritten">Du ändrade din Concord-lista</string> + <plurals name="backup_card_dm_relays_lost"> + <item quantity="one">Du tog bort %1$d DM-relä</item> + <item quantity="other">Du tog bort %1$d DM-reläer</item> + </plurals> + <plurals name="backup_card_feeds_removed"> + <item quantity="one">Du tog bort %1$d favoritflöde</item> + <item quantity="other">Du tog bort %1$d favoritflöden</item> + </plurals> + <plurals name="backup_card_follows_dropped"> + <item quantity="one">Du slutade följa %1$d person</item> + <item quantity="other">Du slutade följa %1$d personer</item> + </plurals> + <plurals name="backup_card_hashtags_unfollowed"> + <item quantity="one">Du slutade följa %1$d hashtagg</item> + <item quantity="other">Du slutade följa %1$d hashtaggar</item> + </plurals> + <plurals name="backup_card_indexer_relays_lost"> + <item quantity="one">Du tog bort %1$d indexerande relä</item> + <item quantity="other">Du tog bort %1$d indexerande reläer</item> + </plurals> + <plurals name="backup_card_key_package_relays_lost"> + <item quantity="one">Du tog bort %1$d nyckelrelä för gruppchattar</item> + <item quantity="other">Du tog bort %1$d nyckelreläer för gruppchattar</item> + </plurals> + <plurals name="backup_card_leave_chats"> + <item quantity="one">Du lämnade %1$d offentlig chatt</item> + <item quantity="other">Du lämnade %1$d offentliga chattar</item> + </plurals> + <plurals name="backup_card_leave_communities"> + <item quantity="one">Du lämnade %1$d gemenskap</item> + <item quantity="other">Du lämnade %1$d gemenskaper</item> + </plurals> + <plurals name="backup_card_leave_groups"> + <item quantity="one">Du lämnade %1$d grupp</item> + <item quantity="other">Du lämnade %1$d grupper</item> + </plurals> + <plurals name="backup_card_leave_rooms"> + <item quantity="one">Du lämnade %1$d chattrum</item> + <item quantity="other">Du lämnade %1$d chattrum</item> + </plurals> + <plurals name="backup_card_mutes_visible"> + <item quantity="one">Du slutade tysta %1$d sak</item> + <item quantity="other">Du slutade tysta %1$d saker</item> + </plurals> + <plurals name="backup_card_nip65_lost"> + <item quantity="one">Du tog bort %1$d offentligt relä</item> + <item quantity="other">Du tog bort %1$d offentliga reläer</item> + </plurals> + <string name="backup_card_nutzap_key_removed">Du tog bort din nutzap-nyckel</string> + <string name="backup_card_nutzap_key_replaced">Du bytte ut din nutzap-nyckel</string> + <plurals name="backup_card_nutzap_mints_removed"> + <item quantity="one">Du tog bort %1$d nutzap-mint</item> + <item quantity="other">Du tog bort %1$d nutzap-mints</item> + </plurals> + <plurals name="backup_card_offers_removed"> + <item quantity="one">Du tog bort %1$d BOLT12-erbjudande</item> + <item quantity="other">Du tog bort %1$d BOLT12-erbjudanden</item> + </plurals> + <plurals name="backup_card_payment_targets_removed"> + <item quantity="one">Du tog bort %1$d sätt att få betalt</item> + <item quantity="other">Du tog bort %1$d sätt att få betalt</item> + </plurals> + <plurals name="backup_card_places_unfollowed"> + <item quantity="one">Du slutade följa %1$d plats</item> + <item quantity="other">Du slutade följa %1$d platser</item> + </plurals> + <plurals name="backup_card_private_outbox_lost"> + <item quantity="one">Du tog bort %1$d privat relä</item> + <item quantity="other">Du tog bort %1$d privata reläer</item> + </plurals> + <plurals name="backup_card_profile_lost_fields"> + <item quantity="one">Du tog bort %1$d profilfält</item> + <item quantity="other">Du tog bort %1$d profilfält</item> + </plurals> + <plurals name="backup_card_relay_feeds_lost"> + <item quantity="one">Du tog bort %1$d reläflöde</item> + <item quantity="other">Du tog bort %1$d reläflöden</item> + </plurals> + <plurals name="backup_card_search_relays_lost"> + <item quantity="one">Du tog bort %1$d sökrelä</item> + <item quantity="other">Du tog bort %1$d sökreläer</item> + </plurals> + <plurals name="backup_card_trust_services_changed"> + <item quantity="one">Du bytte leverantör för %1$d förtroendetjänst</item> + <item quantity="other">Du bytte leverantör för %1$d förtroendetjänster</item> + </plurals> + <plurals name="backup_card_trusted_lost"> + <item quantity="one">Du slutade lita på %1$d relä</item> + <item quantity="other">Du slutade lita på %1$d reläer</item> + </plurals> + <plurals name="backup_card_unblocked"> + <item quantity="one">Du slutade blockera %1$d relä</item> + <item quantity="other">Du slutade blockera %1$d reläer</item> + </plurals> + <string name="backup_card_wallet_emptied">Du tömde din Cashu-plånbok</string> + <string name="backup_card_wallet_rewritten">Du ändrade din Cashu-plånbok</string> + <string name="backup_conflict_added">Tillagt (%1$s)</string> + <string name="backup_conflict_changed">Ändrat (%1$s)</string> + <string name="backup_conflict_count_added">Tillagda: %1$s</string> + <string name="backup_conflict_count_changed">Ändrade: %1$s</string> + <string name="backup_conflict_count_removed">Borttagna: %1$s</string> + <string name="backup_conflict_keep_new">Använd ny version</string> + <string name="backup_conflict_private_added">Du lade till privata (krypterade) objekt.</string> + <string name="backup_conflict_private_changed">Du ändrade även privata (krypterade) objekt. De kan inte jämföras här.</string> + <string name="backup_conflict_private_cleared">Du tog bort alla privata (krypterade) objekt.</string> + <string name="backup_conflict_relay_read_only">endast läsning</string> + <string name="backup_conflict_relay_read_write">läsning och skrivning</string> + <string name="backup_conflict_relay_write_only">endast skrivning</string> + <string name="backup_conflict_removed">Borttaget (%1$s)</string> + <string name="backup_conflict_restore_mine">Återställ sparad version</string> + <string name="backup_conflict_title">Ändring i en annan app: %1$s</string> + <string name="backup_entry_algo_feed">Flöden</string> + <string name="backup_entry_chat_room">Chattrum</string> + <string name="backup_entry_community">Gemenskaper</string> + <string name="backup_entry_hashtag">Hashtaggar</string> + <string name="backup_entry_identity_claim">Länkade identiteter</string> + <string name="backup_entry_location">Platser</string> + <string name="backup_entry_mint">Mints</string> + <string name="backup_entry_nutzap_key">Nutzap-nycklar</string> + <string name="backup_entry_offer">Erbjudanden</string> + <string name="backup_entry_other">Övriga poster</string> + <string name="backup_entry_payment_target">Betalningsadresser</string> + <string name="backup_entry_person">Personer</string> + <string name="backup_entry_profile_field">Profilfält</string> + <string name="backup_entry_public_chat">Offentliga chattar</string> + <string name="backup_entry_relay">Reläer</string> + <string name="backup_entry_relay_group">Grupper</string> + <string name="backup_entry_thread">Trådar</string> + <string name="backup_entry_trust_provider">Förtroendeleverantörer</string> + <string name="backup_entry_word">Ord</string> + <string name="backup_profile_field_about">Om</string> + <string name="backup_profile_field_banner">Banner</string> + <string name="backup_profile_field_birthday">Födelsedag</string> + <string name="backup_profile_field_bot">Bot-markering</string> + <string name="backup_profile_field_clink_offer">CLINK-erbjudande</string> + <string name="backup_profile_field_display_name">Visningsnamn</string> + <string name="backup_profile_field_lud06">LNURL</string> + <string name="backup_profile_field_lud16">Lightning-adress</string> + <string name="backup_profile_field_name">Användarnamn</string> + <string name="backup_profile_field_nip05">Nostr-adress</string> + <string name="backup_profile_field_picture">Bild</string> + <string name="backup_profile_field_pronouns">Pronomen</string> + <string name="backup_profile_field_website">Webbplats</string> + <string name="backup_review_allowed_tag">Tillåten</string> + <string name="backup_review_changed_by_other_app">Ändrad i en annan app · %1$s</string> + <string name="backup_review_concord_body">Dina Concord-gemenskaper är krypterade till dig, så de kan inte jämföras här. En återställning lägger tillbaka exakt det som den här enheten sparade.</string> + <string name="backup_review_demoted_tag">Flyttad</string> + <string name="backup_review_dropped_count">Borttagna: %1$s</string> + <string name="backup_review_dropped_tag">Borttagen</string> + <string name="backup_review_encrypted_added">Du skapade den</string> + <string name="backup_review_encrypted_changed">Du ändrade den</string> + <string name="backup_review_encrypted_cleared">Du tömde den</string> + <string name="backup_review_feed">Flöde</string> + <string name="backup_review_gone">Borta</string> + <string name="backup_review_hashtags_you_follow">Hashtaggar du följer</string> + <string name="backup_review_inbox">Inkorg</string> + <string name="backup_review_inbox_explainer">där andra når dig</string> + <string name="backup_review_joined">gick med</string> + <string name="backup_review_kept">behållna</string> + <string name="backup_review_kept_count">Behållna: %1$s</string> + <string name="backup_review_key_removed">Mottagarnyckel borttagen</string> + <string name="backup_review_key_replaced">Mottagarnyckel utbytt</string> + <string name="backup_review_key_warning">Nutzaps kommer att låsas till den här nya nyckeln. Om den här enheten inte har den kanske du inte kan lösa in dem här.</string> + <string name="backup_review_left">lämnade</string> + <string name="backup_review_left_tag">Lämnad</string> + <string name="backup_review_listed_tag">Listad</string> + <string name="backup_review_moved_tag">Flyttad</string> + <string name="backup_review_names_and_more">%1$s och %2$s till</string> + <string name="backup_review_new">Ny version</string> + <string name="backup_review_new_count">Nya: %1$s</string> + <string name="backup_review_new_count_caption">nya</string> + <string name="backup_review_new_tag">Ny</string> + <string name="backup_review_newly_blocked">Nyligen blockerade</string> + <string name="backup_review_newly_muted">Nyligen tystade: %1$s</string> + <string name="backup_review_no_longer_blocked">Inte längre blockerade</string> + <string name="backup_review_not_set">Inte angivet</string> + <string name="backup_review_nothing_here">Inget här</string> + <string name="backup_review_offer">BOLT12-erbjudande</string> + <plurals name="backup_review_offers_gone"> + <item quantity="one">%1$d erbjudande borttaget</item> + <item quantity="other">%1$d erbjudanden borttagna</item> + </plurals> + <string name="backup_review_outbox">Utkorg</string> + <string name="backup_review_outbox_explainer">där du publicerar</string> + <plurals name="backup_review_payment_targets_gone"> + <item quantity="one">%1$d sätt att betala dig borttaget</item> + <item quantity="other">%1$d sätt att betala dig borttagna</item> + </plurals> + <string name="backup_review_payments_explainer">De som betalar dig ser bara det som finns i den här listan.</string> + <string name="backup_review_places_gone">platser borta</string> + <string name="backup_review_places_you_follow">Platser du följer</string> + <string name="backup_review_private_added_body">De är krypterade nu, vilket troligen är vart de offentliga objekten ovan tog vägen.</string> + <string name="backup_review_private_added_title">Du gjorde objekt privata</string> + <string name="backup_review_private_changed_body">De är krypterade, så de kan inte jämföras här.</string> + <string name="backup_review_private_changed_title">Du ändrade privata objekt</string> + <string name="backup_review_private_wiped_body">De är krypterade, så bara du kan se vad de var. En återställning tar tillbaka alla.</string> + <string name="backup_review_private_wiped_title">Du tog bort alla privata objekt</string> + <string name="backup_review_receiving_tag">Tar emot</string> + <string name="backup_review_renamed">omdöpt</string> + <string name="backup_review_renamed_tag">Omdöpt</string> + <string name="backup_review_saved">På den här enheten</string> + <plurals name="backup_review_search_people"> + <item quantity="one">Sök bland %1$s person</item> + <item quantity="other">Sök bland %1$s personer</item> + </plurals> + <string name="backup_review_searching_tag">Söker</string> + <string name="backup_review_service">Tjänst</string> + <string name="backup_review_service_followers">Följare</string> + <string name="backup_review_service_rank">Rang</string> + <string name="backup_review_service_topics">Ämnen</string> + <plurals name="backup_review_services_changed"> + <item quantity="one">%1$s av %2$s tjänst ändrad</item> + <item quantity="other">%1$s av %2$s tjänster ändrade</item> + </plurals> + <string name="backup_review_services_changed_explainer">De kommer nu från någon annan, eller från ingen</string> + <string name="backup_review_still_blocked">Fortfarande blockerade</string> + <string name="backup_review_storing_tag">Lagrar</string> + <string name="backup_review_tab_dropped">Borttagna · %1$s</string> + <string name="backup_review_tab_edited">Redigerade · %1$s</string> + <string name="backup_review_tab_new">Nya · %1$s</string> + <string name="backup_review_title_cleared">%1$s har tömts</string> + <string name="backup_review_title_updated">%1$s har uppdaterats</string> + <string name="backup_review_topics_gone">ämnen borta</string> + <string name="backup_review_trust_warning">Rang, antal följare och skräppostfiltrering som du ser kommer från de här leverantörerna.</string> + <string name="backup_review_trusted_tag">Betrodd</string> + <string name="backup_review_unblocked">Blockering hävd: %1$s</string> + <string name="backup_review_unblocked_explainer">Amethyst ansluter till dem igen</string> + <string name="backup_review_units_tag">Enheter</string> + <string name="backup_review_unmuted">Tystnad hävd: %1$s</string> + <string name="backup_review_unmuted_explainer">De här kan nå dina flöden igen</string> + <string name="backup_review_untrusted_tag">Ej betrodd</string> + <string name="backup_review_wallet_body">Din plånboks mints och nycklar är krypterade till dig, så de kan inte jämföras objekt för objekt här. En återställning lägger tillbaka exakt det som den här enheten sparade.</string> + <string name="backup_review_was_named">hette ”%1$s”</string> + <string name="backup_review_words_and_hashtags">Ord & hashtaggar</string> + <string name="backup_type_app_settings">synkade appinställningar</string> + <string name="backup_type_app_settings_explainer">Amethyst-inställningar som synkas mellan dina enheter. De är krypterade, så bara att de finns kan jämföras.</string> + <string name="backup_type_blocked_relays">lista över blockerade reläer</string> + <string name="backup_type_blocked_relays_explainer">Reläer som den här appen aldrig ansluter till.</string> + <string name="backup_type_bolt12_offers">BOLT12-erbjudanden</string> + <string name="backup_type_bolt12_offers_explainer">Lightning-erbjudandena som andra använder för att betala dig.</string> + <string name="backup_type_cashu_wallet">Cashu-plånbok</string> + <string name="backup_type_cashu_wallet_explainer">Dina inställningar och mints för ecash-plånboken. De är krypterade, så bara att de finns kan jämföras.</string> + <string name="backup_type_communities">gemenskapslista</string> + <string name="backup_type_communities_explainer">Gemenskaperna du gått med i.</string> + <string name="backup_type_concord_communities">Concord-gemenskapslista</string> + <string name="backup_type_concord_communities_explainer">Concord-gemenskaperna du gått med i. De är krypterade, så bara att de finns kan jämföras.</string> + <string name="backup_type_dm_relays">DM-relälista</string> + <string name="backup_type_dm_relays_explainer">Dit andra levererar dina privata meddelanden.</string> + <string name="backup_type_ephemeral_chats">lista över tillfälliga chattar</string> + <string name="backup_type_ephemeral_chats_explainer">De tillfälliga chattrummen du gått med i.</string> + <string name="backup_type_favorite_algo_feeds">lista över favoritflöden</string> + <string name="backup_type_favorite_algo_feeds_explainer">De algoritmiska flödena du har sparat.</string> + <string name="backup_type_follow_list">följlista</string> + <string name="backup_type_follow_list_explainer">Avgör vems inlägg som visas i ditt hemflöde.</string> + <string name="backup_type_geohashes">följda platser</string> + <string name="backup_type_geohashes_explainer">Platser vars inlägg visas i dina flöden.</string> + <string name="backup_type_hashtags">följda hashtaggar</string> + <string name="backup_type_hashtags_explainer">Hashtaggar vars inlägg visas i dina flöden.</string> + <string name="backup_type_indexer_relays">lista över indexerande reläer</string> + <string name="backup_type_indexer_relays_explainer">Reläerna som används för att hitta andras profiler och relälistor.</string> + <string name="backup_type_key_package_relays">nyckelreläer för gruppchattar</string> + <string name="backup_type_key_package_relays_explainer">Där andra hittar nycklarna för att lägga till dig i krypterade gruppchattar.</string> + <string name="backup_type_mute_list">tystnadslista</string> + <string name="backup_type_mute_list_explainer">Döljer personer, ord, hashtaggar och trådar från allt du ser.</string> + <string name="backup_type_nutzap_info">inställningar för nutzap-mottagning</string> + <string name="backup_type_nutzap_info_explainer">Mints och nyckel som andra använder för att skicka nutzaps till dig.</string> + <string name="backup_type_other">lista</string> + <string name="backup_type_other_explainer">En lista som sparats på den här enheten.</string> + <string name="backup_type_outbox_inbox_relays">offentlig relälista</string> + <string name="backup_type_outbox_inbox_relays_explainer">Talar om för alla var dina inlägg finns och vart svar och omnämnanden till dig ska skickas.</string> + <string name="backup_type_payment_targets">betalningsadresser</string> + <string name="backup_type_payment_targets_explainer">Sätten som andra kan betala dig på.</string> + <string name="backup_type_private_outbox_relays">privat relälista</string> + <string name="backup_type_private_outbox_relays_explainer">Där dina utkast och privata data lagras.</string> + <string name="backup_type_profile">profil</string> + <string name="backup_type_profile_explainer">Ditt namn, din bild, din bio och dina betalningsadresser, så som alla ser dem.</string> + <string name="backup_type_public_chats">lista över offentliga chattar</string> + <string name="backup_type_public_chats_explainer">De offentliga chattarna du gått med i.</string> + <string name="backup_type_relay_feeds">favoritreläflöden</string> + <string name="backup_type_relay_feeds_explainer">Reläerna du bläddrar i som flöden.</string> + <string name="backup_type_relay_groups">grupplista</string> + <string name="backup_type_relay_groups_explainer">De relä-baserade grupperna du gått med i.</string> + <string name="backup_type_search_relays">lista över sökreläer</string> + <string name="backup_type_search_relays_explainer">Reläerna som används när du söker.</string> + <string name="backup_type_trust_providers">lista över förtroendeleverantörer</string> + <string name="backup_type_trust_providers_explainer">Tjänsterna du litar på för att rangordna och verifiera personer åt dig.</string> + <string name="backup_type_trusted_relays">lista över betrodda reläer</string> + <string name="backup_type_trusted_relays_explainer">Reläer du litar på att ansluta till utan att fråga.</string> </resources> From fbd710f9cffabe3d05247900357928f8247ee6fb Mon Sep 17 00:00:00 2001 From: davotoula <david.kaspar@gmail.com> Date: Sun, 27 Sep 2026 11:16:29 +0200 Subject: [PATCH 47/60] refactor(geocache): name the hunt's cache count instead of a let chain --- .../screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.kt index 48fb28b80e..7028c9f23f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geocaches/detail/GeocacheAddToHuntSheet.kt @@ -179,7 +179,8 @@ fun GeocacheAddToHuntSheet( if (already) { stringRes(Res.string.geocache_hunt_already_on) } else { - hunt.geocaches().size.let { pluralStringRes(Res.plurals.geocache_hunt_caches, it, it) } + val cacheCount = hunt.geocaches().size + pluralStringRes(Res.plurals.geocache_hunt_caches, cacheCount, cacheCount) }, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, From d993ee8abc972a1e3b1bc3ef1a3dcca1f7938a11 Mon Sep 17 00:00:00 2001 From: davotoula <1747287+davotoula@users.noreply.github.com> Date: Sun, 27 Sep 2026 12:00:17 +0000 Subject: [PATCH 48/60] chore: sync Crowdin translations and seed translator npub placeholders --- amethyst/src/main/res/values-cs/strings.xml | 8 +- .../src/main/res/values-de-rDE/strings.xml | 8 +- .../src/main/res/values-pt-rBR/strings.xml | 8 +- .../src/main/res/values-sv-rSE/strings.xml | 8 +- .../composeResources/values-cs/strings.xml | 1286 ++++++++--------- .../values-de-rDE/strings.xml | 1206 ++++++++-------- .../values-hi-rIN/strings.xml | 40 +- .../values-hu-rHU/strings.xml | 519 ++++++- .../values-pl-rPL/strings.xml | 232 +++ .../values-pt-rBR/strings.xml | 1206 ++++++++-------- .../values-sv-rSE/strings.xml | 1206 ++++++++-------- docs/changelog/translators.json | 12 +- 12 files changed, 3230 insertions(+), 2509 deletions(-) diff --git a/amethyst/src/main/res/values-cs/strings.xml b/amethyst/src/main/res/values-cs/strings.xml index 7130fe54f3..0d68f74921 100644 --- a/amethyst/src/main/res/values-cs/strings.xml +++ b/amethyst/src/main/res/values-cs/strings.xml @@ -192,6 +192,9 @@ <string name="share_target_as_picture">Nový obrázek</string> <string name="share_target_as_short_video">Nové krátké video</string> <string name="share_target_as_video">Nové video</string> + <string name="share_target_scan_qr">Skenovat QR kód</string> + <string name="shortcut_scan_qr_short">Skenovat QR</string> + <string name="shortcut_scan_qr_long">Naskenovat QR kód</string> <string name="new_highlight_passage_label">Zvýrazněný text</string> <!-- Label for the mixed media feed: pictures + every NIP-71 video kind + NIP-94 files. Distinct from the "shorts" string, which labels the vertical/short-video-only feed. --> @@ -311,6 +314,7 @@ <item quantity="other">Těžení proof of work… (%1$d příspěvků ve frontě)</item> </plurals> <string name="pow_mining_title">Těžení proof of work</string> + <string name="pow_notification_sending">Odesílání na relé…</string> <string name="pow_notification_cancel_all">Zrušit</string> <string name="pow_time_left">≈ %1$s zbývá</string> <string name="pow_time_left_soon">každou chvíli</string> @@ -377,8 +381,4 @@ <string name="mute_button">Zvuk zapnutý. Klikněte pro ztlumení</string> <string name="play">Hrát</string> <string name="pause">Pozastavit</string> - <string name="share_target_scan_qr">Skenovat QR kód</string> - <string name="shortcut_scan_qr_short">Skenovat QR</string> - <string name="shortcut_scan_qr_long">Naskenovat QR kód</string> - <string name="pow_notification_sending">Odesílání na relé…</string> </resources> diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 2e4d479622..5a488067cf 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -180,6 +180,9 @@ <string name="share_target_as_picture">Neues Bild</string> <string name="share_target_as_short_video">Neuer Kurzfilm</string> <string name="share_target_as_video">Neues Video</string> + <string name="share_target_scan_qr">QR-Code scannen</string> + <string name="shortcut_scan_qr_short">QR scannen</string> + <string name="shortcut_scan_qr_long">Einen QR-Code scannen</string> <string name="new_highlight_passage_label">Hervorgehobener Text</string> <!-- Label for the mixed media feed: pictures + every NIP-71 video kind + NIP-94 files. Distinct from the "shorts" string, which labels the vertical/short-video-only feed. --> @@ -291,6 +294,7 @@ <item quantity="other">Arbeitsnachweis wird geschürft… (%1$d Beiträge in der Warteschlange)</item> </plurals> <string name="pow_mining_title">Arbeitsnachweis wird geschürft</string> + <string name="pow_notification_sending">Wird an Relays gesendet…</string> <string name="pow_notification_cancel_all">Abbrechen</string> <string name="pow_time_left">≈ %1$s verbleibend</string> <string name="pow_time_left_soon">jeden Moment</string> @@ -349,8 +353,4 @@ <string name="mute_button">Ton ein. Zur Stummschaltung drücken</string> <string name="play">Abspielen</string> <string name="pause">Pausen</string> - <string name="share_target_scan_qr">QR-Code scannen</string> - <string name="shortcut_scan_qr_short">QR scannen</string> - <string name="shortcut_scan_qr_long">Einen QR-Code scannen</string> - <string name="pow_notification_sending">Wird an Relays gesendet…</string> </resources> diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index af84b30244..0cc858952f 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -180,6 +180,9 @@ <string name="share_target_as_picture">Nova Foto</string> <string name="share_target_as_short_video">Novo Vídeo Curto</string> <string name="share_target_as_video">Novo Vídeo</string> + <string name="share_target_scan_qr">Escanear código QR</string> + <string name="shortcut_scan_qr_short">Ler QR</string> + <string name="shortcut_scan_qr_long">Escanear um código QR</string> <string name="new_highlight_passage_label">Texto destacado</string> <!-- Label for the mixed media feed: pictures + every NIP-71 video kind + NIP-94 files. Distinct from the "shorts" string, which labels the vertical/short-video-only feed. --> @@ -291,6 +294,7 @@ <item quantity="other">Minerando prova de trabalho… (%1$d publicações na fila)</item> </plurals> <string name="pow_mining_title">Minerando prova de trabalho</string> + <string name="pow_notification_sending">Enviando para relays…</string> <string name="pow_notification_cancel_all">Cancelar</string> <string name="pow_time_left">≈ %1$s restante</string> <string name="pow_time_left_soon">a qualquer momento</string> @@ -349,8 +353,4 @@ <string name="mute_button">Som ligado. Clique para silenciar</string> <string name="play">Reproduzir</string> <string name="pause">Pausar</string> - <string name="share_target_scan_qr">Escanear código QR</string> - <string name="shortcut_scan_qr_short">Ler QR</string> - <string name="shortcut_scan_qr_long">Escanear um código QR</string> - <string name="pow_notification_sending">Enviando para relays…</string> </resources> diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 294ee30c6f..4d6749fdb5 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -180,6 +180,9 @@ <string name="share_target_as_picture">Ny bild</string> <string name="share_target_as_short_video">Ny kortfilm</string> <string name="share_target_as_video">Ny video</string> + <string name="share_target_scan_qr">Skanna QR-kod</string> + <string name="shortcut_scan_qr_short">Skanna QR</string> + <string name="shortcut_scan_qr_long">Skanna en QR-kod</string> <string name="new_highlight_passage_label">Markerad text</string> <!-- Label for the mixed media feed: pictures + every NIP-71 video kind + NIP-94 files. Distinct from the "shorts" string, which labels the vertical/short-video-only feed. --> @@ -291,6 +294,7 @@ <item quantity="other">Minar bevis på arbete… (%1$d inlägg i kön)</item> </plurals> <string name="pow_mining_title">Minar bevis på arbete</string> + <string name="pow_notification_sending">Skickar till reläer…</string> <string name="pow_notification_cancel_all">Avbryt</string> <string name="pow_time_left">≈ %1$s kvar</string> <string name="pow_time_left_soon">när som helst nu</string> @@ -349,8 +353,4 @@ <string name="mute_button">Ljud på. Klicka för att stänga av ljudet</string> <string name="play">Spela</string> <string name="pause">Pausa</string> - <string name="share_target_scan_qr">Skanna QR-kod</string> - <string name="shortcut_scan_qr_short">Skanna QR</string> - <string name="shortcut_scan_qr_long">Skanna en QR-kod</string> - <string name="pow_notification_sending">Skickar till reläer…</string> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-cs/strings.xml b/commonsUI/src/commonMain/composeResources/values-cs/strings.xml index eb86dff2d2..06b182ff77 100644 --- a/commonsUI/src/commonMain/composeResources/values-cs/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-cs/strings.xml @@ -174,6 +174,156 @@ <string name="road_event_traffic_jam">Dopravní zácpa</string> <string name="road_event_unknown">Silniční událost</string> <!-- NIP-CC geocaching (kinds 37516 / 7516) --> + <string name="geocache_unnamed">Keš</string> + <string name="geocache_type_traditional">Tradiční</string> + <string name="geocache_type_multi">Multi-keš</string> + <string name="geocache_type_mystery">Mystery</string> + <string name="geocache_size_micro">Mikro</string> + <string name="geocache_size_small">Malá</string> + <string name="geocache_size_regular">Normální</string> + <string name="geocache_size_large">Velká</string> + <string name="geocache_size_other">Jiná velikost</string> + <string name="geocache_difficulty_short">D%1$d</string> + <string name="geocache_terrain_short">T%1$d</string> + <string name="geocache_first_to_find">První nálezce</string> + <string name="geocache_is_art">Umělecké dílo</string> + <string name="geocache_mission">Key Quest</string> + <string name="geocache_needs_verification">Ověřené nálezy</string> + <string name="geocache_claimed">Obsazeno</string> + <string name="geocache_archived">Archivováno</string> + <string name="geocache_hint_tap_to_reveal">Nevíte si rady? Klepnutím zobrazíte nápovědu</string> + <string name="geocache_hint_other_reading">Nečitelné? Klepnutím zobrazíte druhé čtení</string> + <string name="geocache_nearby">Poblíž</string> + <string name="geocache_found_it">Nalezeno!</string> + <string name="geocache_verified_find">Ověřený nález</string> + <string name="geocache_invalid_proof">Důkaz nesouhlasí</string> + <string name="geocache_loading">Načítání keše…</string> + <string name="geocache_hidden_by">Ukryl %1$s</string> + <string name="geocache_navigate">Navigovat</string> + <string name="geocache_copy_coordinates">Kopírovat souřadnice</string> + <string name="geocache_coordinates_copied">Souřadnice zkopírovány</string> + <string name="geocache_photos_section">Fotky</string> + <string name="geocache_mission_section">Mise Key Quest</string> + <string name="geocache_logs_section">Logy</string> + <string name="geocache_no_logs">Tuto keš zatím nikdo nezalogoval. Buďte první.</string> + <string name="geocache_i_found_it">Nalezeno</string> + <string name="geocache_didnt_find_it">Nenalezeno</string> + <string name="geocache_add_note">Přidat poznámku</string> + <string name="geocache_needs_maintenance">Nahlásit potřebu údržby</string> + <string name="geocache_add_to_hunt">Přidat do výpravy</string> + <string name="geocache_hunt_new">Nová výprava…</string> + <string name="geocache_hunt_none_yet">Zatím jste nevytvořili žádnou výpravu.</string> + <string name="geocache_hunt_already_on">Už je v této výpravě</string> + <plurals name="geocache_caches_here"> + <item quantity="one">%1$d keš zde</item> + <item quantity="few">%1$d keše zde</item> + <item quantity="many">%1$d keše zde</item> + <item quantity="other">%1$d keší zde</item> + </plurals> + <string name="geocache_photo_remove">Odebrat tuto fotku</string> + <string name="geocache_archived_notice">Tato keš byla archivována. Už není ve hře.</string> + <string name="geocache_ftf_won_by">Prvním nálezcem se stal %1$s</string> + <string name="geocache_ftf_available">Místo prvního nálezce je stále volné</string> + <string name="geocache_owner_actions">Vlastník</string> + <string name="geocache_owner_edit">Upravit tuto keš</string> + <string name="geocache_owner_archive">Archivovat tuto keš</string> + <string name="geocache_owner_archive_confirm">Archivovat. Nálezci uvidí, že už není ve hře, a listing zůstane čitelný.</string> + <string name="geocache_owner_lock_ftf">Potvrdit prvního nálezce</string> + <string name="geocache_owner_lock_ftf_explain">Zveřejní tag F se jménem %1$s. Je to trvalé.</string> + <string name="geocache_owner_no_finders">Zatím žádné ověřené nálezy, takže není koho potvrdit.</string> + <string name="geocache_owner_show_qr">Zobrazit ověřovací kód</string> + <string name="geocache_owner_qr_warning">Vytiskněte ho a vložte do keše. Amethyst ho neukládá — pokud ho ztratíte, nálezci už nebudou moci prokázat, že tam byli.</string> + <string name="geocache_log_type_dnf">Nenalezeno</string> + <string name="geocache_log_type_note">Poznámka</string> + <string name="geocache_log_type_maintenance">Potřebuje údržbu</string> + <string name="geocache_log_title">Zapsat nález</string> + <string name="geocache_log_cache_label">Keš</string> + <string name="geocache_log_your_log">Váš log</string> + <string name="geocache_log_placeholder">Jak to šlo?</string> + <string name="geocache_log_add_photo">Přidat fotku</string> + <string name="geocache_log_mission_answer">Vaše odpověď Key Quest</string> + <string name="geocache_log_proof_section">Důkaz přítomnosti</string> + <string name="geocache_log_scan_code">Naskenujte kód v keši</string> + <string name="geocache_log_scan_again">Naskenovat jiný kód</string> + <string name="geocache_log_proof_verified">Ověřeno — byli jste u keše</string> + <string name="geocache_log_proof_explain">Jednou podepsáno kódem z krabičky. Klíč se nikdy neukládá a nikdy neopustí tuto obrazovku.</string> + <string name="geocache_log_proof_bad_code">To není ověřovací kód.</string> + <string name="geocache_log_proof_wrong_cache">Tento kód patří jiné keši.</string> + <string name="geocache_log_proof_optional">Tato keš nevyžaduje důkaz, takže ji můžete rovnou zalogovat.</string> + <string name="geocache_log_no_location_attached">K tomuto logu není připojena vaše vlastní poloha.</string> + <string name="geocache_log_post">Odeslat</string> + <string name="geocache_log_comment_title">Přidat log</string> + <string name="geocache_tab_nearby">Poblíž</string> + <string name="geocache_tab_map">Mapa</string> + <string name="geocache_tab_hunts">Výpravy</string> + <string name="geocache_tab_finds">Nálezy</string> + <string name="geocache_tab_mine">Moje</string> + <string name="geocache_filter_unfound">Nenalezené</string> + <string name="geocache_filter_difficulty">D ≤ %1$d</string> + <string name="geocache_filter_terrain">T ≤ %1$d</string> + <string name="geocache_filter_has_prize">S cenou</string> + <string name="geocache_filter_verified_only">Vyžaduje důkaz</string> + <string name="geocache_use_my_location">Použít mou polohu</string> + <string name="geocache_sorted_by_distance">Seřazeno podle vzdálenosti</string> + <string name="geocache_sorted_by_recent">Nejnovější první</string> + <string name="geocache_hide_a_cache">Ukrýt keš</string> + <string name="geocache_empty_nearby">Zatím tu nejsou žádné keše. Zkuste širší seznam sledovaných, nebo ukryjte první.</string> + <string name="geocache_empty_finds">Zatím jste nezapsali žádný nález.</string> + <string name="geocache_empty_mine">Zatím jste neukryli žádnou keš.</string> + <string name="geocache_empty_hunts">Zatím žádné výpravy.</string> + <string name="geocache_stat_finds">Nálezy</string> + <string name="geocache_stat_ftf">První nálezy</string> + <string name="geocache_stat_verified">Ověřené</string> + <string name="geocache_stat_hidden">Ukryté</string> + <string name="geocache_map_recenter">Vycentrovat na mě</string> + <string name="geocache_map_long_press">Dlouhým stiskem na mapě tam ukryjete keš</string> + <string name="geocache_map_open">Otevřít</string> + <string name="geocache_new_section_where">Kde</string> + <string name="geocache_new_section_what">Co</string> + <string name="geocache_new_section_extras">Doplňky</string> + <string name="geocache_new_section_proof">Důkaz</string> + <string name="geocache_new_section_relays">Relaye</string> + <string name="geocache_new_location_warning">Tímto zveřejníte veřejnou polohu s přesností asi na 5 metrů. Uvidí ji kdokoli na Nostru a zveřejněnou událost nelze vzít zpět.</string> + <string name="geocache_new_pick_location">Vyberte místo</string> + <string name="geocache_new_ladder">Zveřejní %1$s a k tomu hrubší buňky až po 3 znaky pro vyhledávání v okolí.</string> + <string name="geocache_new_name">Název</string> + <string name="geocache_new_description">Popis</string> + <string name="geocache_new_type">Typ</string> + <string name="geocache_new_size">Velikost</string> + <string name="geocache_new_difficulty">Obtížnost</string> + <string name="geocache_new_terrain">Terén</string> + <string name="geocache_new_hint">Nápověda</string> + <string name="geocache_new_hint_preview">Nálezci uvidí</string> + <string name="geocache_new_hint_scramble">Zašifrovat nápovědu (ROT13)</string> + <string name="geocache_new_mission">Mise Key Quest</string> + <string name="geocache_new_modifiers">Modifikátory</string> + <string name="geocache_new_require_proof">Vyžadovat důkaz přítomnosti</string> + <string name="geocache_new_proof_ready">Pro tuto keš byl vygenerován ověřovací klíč.</string> + <string name="geocache_new_log_relays">Kam mají nálezci logovat</string> + <string name="geocache_new_publish">Zveřejnit</string> + <string name="geocache_new_needs_name">Pojmenujte keš.</string> + <string name="geocache_new_needs_location">Vyberte, kde je keš ukrytá.</string> + <string name="geocache_hunt_progress">Nalezeno %1$d z %2$d</string> + <string name="geocache_hunt_start">Zahájit výpravu</string> + <plurals name="geocache_hunt_caches"> + <item quantity="one">%1$d keš</item> + <item quantity="few">%1$d keše</item> + <item quantity="many">%1$d keše</item> + <item quantity="other">%1$d keší</item> + </plurals> + <string name="geocache_hunt_title">Název</string> + <string name="geocache_hunt_description">Popis</string> + <string name="geocache_hunt_banner">Obrázek banneru</string> + <string name="geocache_hunt_pick_caches">Keše v pořadí</string> + <string name="geocache_hunt_add_cache">Přidat keš</string> + <string name="geocache_hunt_move_up">Posunout nahoru</string> + <string name="geocache_hunt_move_down">Posunout dolů</string> + <string name="geocache_hunt_remove">Odebrat</string> + <string name="geocache_hunt_theme">Motiv stránky</string> + <string name="geocache_hunt_map_style">Styl mapy</string> + <string name="geocache_hunt_needs_title">Pojmenujte výpravu.</string> + <string name="geocache_hunt_needs_caches">Přidejte alespoň jednu keš.</string> + <string name="geocache_publish_failed">Zveřejnění se nezdařilo. Zkontrolujte připojení k relayům a zkuste to znovu.</string> <string name="podcast_value_zap_split_hint">Zapy na toto se rozdělují mezi:</string> <string name="podcast_value_split_percent">%1$d%%</string> <string name="podcast_value_for_value">Value-for-Value</string> @@ -198,6 +348,53 @@ </plurals> <string name="profile_banner">Banner profilu</string> <string name="point_to_the_qr_code">Nasměrujte na QR kód</string> + <string name="qr_scanner_camera_rationale">Amethyst potřebuje fotoaparát ke skenování QR kódů. Nic se nenahrává ani neodesílá — snímky se dekódují v zařízení a poté se zahodí.</string> + <string name="qr_scanner_grant_camera">Povolit fotoaparát</string> + <string name="qr_scanner_camera_blocked">Přístup k fotoaparátu je pro Amethyst vypnutý. Znovu ho můžete zapnout v nastavení Androidu.</string> + <string name="qr_scanner_open_settings">Otevřít nastavení</string> + <string name="qr_scanner_unavailable">Dekodér QR kódů se v tomto zařízení nepodařilo spustit. Kód můžete stále vložit jako text.</string> + <string name="qr_scanner_torch_on">Zapnout světlo</string> + <string name="qr_scanner_torch_off">Vypnout světlo</string> + <string name="qr_scanner_dark_hint">Je tma — zkuste světlo</string> + <string name="qr_scanner_scan_image">Naskenovat obrázek</string> + <string name="qr_scanner_paste">Vložit</string> + <string name="qr_scanner_no_code_in_image">V obrázku nebyl nalezen žádný QR kód</string> + <string name="qr_scanner_clipboard_empty">Ve schránce není nic ke skenování</string> + <string name="qr_scanner_pick_one">Klepněte na požadovaný kód</string> + <plurals name="qr_scanner_sequence_progress"> + <item quantity="one">Načteno %1$s z %2$s části — pokračujte</item> + <item quantity="few">Načteno %1$s z %2$s částí — pokračujte</item> + <item quantity="many">Načteno %1$s z %2$s části — pokračujte</item> + <item quantity="other">Načteno %1$s z %2$s částí — pokračujte</item> + </plurals> + <string name="qr_scanner_zoom_reset">Obnovit přiblížení</string> + <string name="qr_scanner_unsupported_title">Amethyst tento kód nedokáže otevřít</string> + <string name="qr_scanner_unsupported_secret">Tento kód obsahuje soukromý klíč nebo tajemství pro spárování, proto se jeho obsah zde nezobrazuje. Použijte obrazovku, která ho očekává.</string> + <string name="qr_scanner_kind_nsec">Toto je soukromý klíč. Vložte ho raději na přihlašovací obrazovce.</string> + <string name="qr_scanner_kind_wallet">Toto je připojení peněženky. Přidejte ho v nastavení peněženky.</string> + <string name="qr_scanner_kind_bunker">Toto je adresa vzdáleného podepisovatele. Amethyst je rozdává, aby ho ostatní aplikace mohly žádat o podpis — přihlásit se s ní nedokáže.</string> + <string name="qr_scanner_kind_signer">Toto je aplikace, která se chce připojit k vašemu podepisovateli. Otevřete ji z obrazovky podepisovatele NIP-46.</string> + <string name="qr_scanner_kind_lightning">Toto je Lightning faktura.</string> + <string name="qr_scanner_kind_cashu">Toto je Cashu token.</string> + <string name="qr_scanner_kind_web">Toto je webový odkaz.</string> + <string name="qr_scanner_kind_text">Toto je prostý text, ne kód Nostr.</string> + <string name="qr_scanner_kind_nostr_unsupported">Toto je kód Nostr, ale tato obrazovka ho nedokáže otevřít.</string> + <string name="qr_scanner_label_profile">Profil</string> + <string name="qr_scanner_label_note">Poznámka</string> + <string name="qr_scanner_label_nostr">Kód Nostr</string> + <string name="qr_scanner_label_relay">Relé</string> + <string name="qr_scanner_label_nsec">Soukromý klíč</string> + <string name="qr_scanner_label_wallet">Připojení peněženky</string> + <string name="qr_scanner_label_bunker">Adresa vzdáleného podepisovatele</string> + <string name="qr_scanner_label_signer">Žádost o připojení k podepisovateli</string> + <string name="qr_scanner_label_lightning">Lightning faktura</string> + <string name="qr_scanner_label_cashu">Cashu token</string> + <string name="qr_scanner_label_web">Webový odkaz</string> + <string name="qr_scanner_label_text">Prostý text</string> + <string name="qr_scanner_open_link">Otevřít odkaz</string> + <string name="qr_scanner_copy">Kopírovat</string> + <string name="qr_scanner_copied">Zkopírováno</string> + <string name="qr_scanner_try_again">Skenovat znovu</string> <string name="show_qr">Zobrazit QR kód</string> <string name="profile_image">Profilový obrázek</string> <string name="your_profile_image">Váš profilový obrázek</string> @@ -648,17 +845,70 @@ <string name="workout_hours">Hodiny</string> <string name="workout_minutes">Minuty</string> <string name="workout_seconds">Sekundy</string> + <string name="workout_suggestion_connect_title">Využijte tréninky, které už máte zaznamenané</string> + <string name="workout_suggestion_connect_message">Připojte Health Connect (Samsung Health, Google Fit, Fitbit, Garmin…) — získáte Moji kondici, vlastní přehled tréninků, a tento trénink vyplníte podle záznamu z hodinek místo ručního psaní každého čísla.</string> <string name="workout_suggestion_connect_button">Připojit</string> <string name="workout_suggestion_distance_km">%1$s km</string> <string name="workout_from_health_connect">Z Health Connect</string> <string name="workout_suggestion_connect_details">Co Amethyst čte</string> <string name="health_connect_rationale_title">Health Connect a Amethyst</string> + <string name="health_connect_rationale_headline">Amethyst používá Health Connect pro funkci Moje kondice — váš vlastní přehled tréninků přímo v telefonu.</string> + <string name="health_connect_rationale_intro">Moje kondice (menu → Vy → Moje kondice) ukazuje, jak trénujete: kolik jste toho udělali tento týden oproti minulému, jak se váš čas dělí mezi běh, cyklistiku, chůzi, plavání a posilovnu, vaše nejlepší výkony, kolik dní jste trénovali a vaši aktuální sérii. Amethyst tento obraz sestaví z tréninků, které vaše hodinky nebo fitness aplikace už uložily do Health Connect, spolu s tréninky, které jste zaznamenali přímo v Amethystu. Vše se vypočítá ve vašem telefonu a zobrazí jen vám.</string> <string name="health_connect_rationale_what_title">Co Amethyst čte a proč</string> + <string name="health_connect_rationale_exercise">Cvičení · jakou aktivitu jste dělali, kdy začala a jak dlouho trvala. To je samotný trénink: určuje počet tréninků, dobu tréninku, rozpis podle aktivit, aktivní dny a vaši sérii.</string> + <string name="health_connect_rationale_distance">Vzdálenost · jak daleko jste se dostali; zobrazuje se jako vzdálenost tento týden oproti minulému, týdenní průměr, vzdálenost podle aktivity a nejdelší vzdálenost.</string> + <string name="health_connect_rationale_calories">Aktivní a celkové kalorie · energie spálená při tréninku; zobrazuje se jako energie tento týden a její srovnání s minulým týdnem. Aktivní kalorie se použijí, pokud je váš zdroj zaznamenává; celkové kalorie slouží jako náhrada u zdrojů, které zaznamenávají jen celkovou energii.</string> + <string name="health_connect_rationale_heart_rate">Tepová frekvence · průměr a maximum během každého tréninku, sloučené do průměrné a nejvyšší tepové frekvence za období. Podle ní poznáte lehký týden od náročného při stejné vzdálenosti.</string> + <string name="health_connect_rationale_steps">Kroky · počet kroků při běhu, chůzi nebo túře; zobrazuje se jako týdenní průměr kroků a nejvyšší počet kroků.</string> + <string name="health_connect_rationale_elevation">Nastoupané převýšení · kolik jste nastoupali; zobrazuje se jako týdenní průměr stoupání a největší stoupání. Právě to odlišuje rovinatý týden od kopcovitého.</string> <string name="health_connect_rationale_limits_title">Co Amethyst nedělá</string> + <string name="health_connect_rationale_limit_window">Čte jen tréninky z posledních 4 týdnů, a to jen když je otevřená Moje kondice nebo editor tréninku. Nikdy nečte na pozadí.</string> <string name="health_connect_rationale_limit_write">Nikdy nic nezapisuje do Health Connect a nikdy nežádá o trasu vašeho cvičení, polohu ani jiný typ zdravotních údajů.</string> + <string name="health_connect_rationale_limit_publish">Vaše zdravotní údaje nikdy nikam neposílá. Moje kondice se počítá ve vašem telefonu a zůstává v něm. Pokud později budete chtít zveřejnit jeden trénink, klepnete u něj na Sdílet, přečtete si příspěvek, který Amethyst vyplnil, a zveřejníte ho sami — vždy jen jeden trénink, nikdy automaticky.</string> + <string name="health_connect_rationale_limit_optional">Celá funkce je volitelná. Oprávnění můžete kdykoli odvolat v Health Connect — Moje kondice se pak vrátí k úvodní výzvě — nebo vypnout návrhy v editoru v Nastavení → Nastavení editoru. Zbytek Amethystu funguje dál.</string> <string name="health_connect_rationale_privacy_policy">Přečíst si celé zásady ochrany soukromí</string> + <string name="my_fitness_title">Moje kondice</string> + <string name="my_fitness_window">Poslední 4 týdny</string> + <string name="my_fitness_loading_metrics">Načítám vzdálenost, kalorie a tepovou frekvenci…</string> + <string name="my_fitness_window_note">Poslední čtyři celé týdny vašeho tréninku: tréninky, které jste zveřejnili, a vše, co Health Connect zaznamenává v tomto telefonu. Amethyst přehled počítá v zařízení a nic z něj nezveřejňuje.</string> + <string name="my_fitness_this_week">Tento týden</string> + <string name="my_fitness_vs_last_week">oproti minulému týdnu</string> + <string name="my_fitness_weekly_average">Týdenní průměr</string> + <string name="my_fitness_workouts">Tréninky</string> + <string name="my_fitness_active_days">Aktivní dny</string> + <string name="my_fitness_streak">Série dní</string> + <string name="my_fitness_time">Čas</string> <!-- Workout duration totals. %1$d is hours, %2$d is minutes; reorder and re-abbreviate freely — these are English abbreviations for hour/minute/second. --> + <string name="my_fitness_duration_hours_minutes">%1$d h %2$d min</string> + <string name="my_fitness_duration_minutes">%1$d min</string> + <string name="my_fitness_duration_seconds">%1$d s</string> + <string name="my_fitness_distance">Vzdálenost</string> + <string name="my_fitness_calories">Kalorie</string> + <string name="my_fitness_steps">Kroky</string> + <string name="my_fitness_elevation">Stoupání</string> + <string name="my_fitness_avg_heart_rate">Prům. TF</string> + <string name="my_fitness_max_heart_rate">Max. TF</string> + <string name="my_fitness_by_activity">Podle aktivity</string> + <string name="my_fitness_bests">Nejlepší výkony</string> + <string name="my_fitness_best_longest_distance">Nejdelší vzdálenost</string> + <string name="my_fitness_best_longest_duration">Nejdelší trénink</string> + <string name="my_fitness_best_biggest_climb">Největší stoupání</string> + <string name="my_fitness_best_most_steps">Nejvíce kroků</string> + <string name="my_fitness_best_highest_heart_rate">Nejvyšší tepová frekvence</string> + <string name="my_fitness_recent">Nedávné tréninky</string> + <string name="my_fitness_empty">Za poslední 4 týdny nic zaznamenáno. Zobrazí se zde tréninky, které zveřejníte, i ty, které zaznamenají vaše hodinky.</string> + <string name="my_fitness_connect_title">Sledujte svůj trénink</string> + <string name="my_fitness_connect_message">Připojte Health Connect a uvidíte týdenní součty, vývoj oproti minulému týdnu, nejlepší výkony a tréninkovou sérii. Vše zůstává ve vašem telefonu.</string> + <string name="my_fitness_connect_banner">Připojte Health Connect a přidejte tréninky zaznamenané hodinkami, včetně tepové frekvence, kroků a stoupání.</string> + <string name="my_fitness_connect_button">Připojit</string> + <string name="my_fitness_share">Sdílet tento trénink</string> + <string name="my_fitness_unit_bpm">tep/min</string> + <string name="my_fitness_unit_kcal">kcal</string> + <string name="my_fitness_unit_km">km</string> + <string name="my_fitness_unit_mi">mi</string> + <string name="my_fitness_unit_m">m</string> + <string name="my_fitness_unit_ft">ft</string> <string name="nsite_none_found">Zatím nenalezeny žádné nSites.</string> <string name="browser_reload">Obnovit</string> <string name="browser_unsupported">Integrovaný prohlížeč vyžaduje Android 11 nebo novější.</string> @@ -1094,6 +1344,8 @@ <string name="poll_closing_date_time">Datum a čas ukončení ankety</string> <string name="poll_closing_in">Anketa končí za %1$s</string> <string name="looking_for_event">Hledá se událost %1$s</string> + <string name="looking_for_event_title">Načítání</string> + <string name="looking_for_private_event">Čekám na načtení vašich soukromých zpráv…</string> <string name="send_zap">Odeslat Zap</string> <string name="custom_zaps_add_a_message">Přidat veřejnou zprávu</string> <string name="custom_zaps_add_a_message_private">Přidat soukromou zprávu</string> @@ -1233,6 +1485,8 @@ <!-- Elapsed call time. %1$d is minutes and %2$d seconds in the short form; in the long form %1$d is hours, %2$d minutes and %3$d seconds. Separator and field order are the translator's to change. --> + <string name="call_duration_minutes_seconds">%1$02d:%2$02d</string> + <string name="call_duration_hours_minutes_seconds">%1$d:%2$02d:%3$02d</string> <string name="call_dismiss">Zavřít</string> <string name="call_switch_camera">Přepnout kameru</string> <string name="call_voice">Hlasový hovor</string> @@ -1367,6 +1621,14 @@ <string name="user_preferences">Uživatelské nastavení</string> <string name="settings_search_placeholder">Hledat nastavení</string> <string name="settings_search_no_results">Pro „%1$s“ nebyla nalezena žádná nastavení</string> + <string name="marmot_preview_group_updated">Skupina aktualizována</string> + <string name="marmot_preview_media">Příloha</string> + <string name="marmot_preview_no_text">Zpráva</string> + <string name="marmot_preview_with_sender">%1$s: %2$s</string> + <string name="marmot_empty_create_action">Vytvořit skupinu</string> + <string name="marmot_invite_device_banner">Pozvánky Marmot chodí na jiné zařízení a zde je nelze otevřít.</string> + <string name="marmot_invite_device_banner_dismiss">Zavřít</string> + <string name="marmot_invite_device_publish">Zveřejnit z tohoto zařízení</string> <string name="reset_marmot_confirm_title">Resetovat stav Marmot?</string> <string name="reset_marmot_confirm_body">Tímto trvale odstraníte všechny skupinové chaty Marmot, historii zpráv a MLS klíče v tomto zařízení pro aktuální účet. Ostatní uživatelé nebudou upozorněni a stále vás mohou vidět ve skupinách až do jejich dalšího potvrzení. Tuto akci nelze vrátit zpět. Nový KeyPackage bude publikován při příští synchronizaci aplikace.</string> <string name="reset_marmot_confirm_action">Resetovat</string> @@ -1641,6 +1903,7 @@ <string name="wallet_no_transactions_for_filter">Žádné transakce neodpovídají tomuto filtru</string> <string name="wallet_loading">Načítání…</string> <string name="wallet_incoming">Přijato</string> + <string name="wallet_open_zapped_note">Otevřít poznámku se zapem</string> <string name="wallet_outgoing">Odesláno</string> <string name="wallet_refresh">Obnovit</string> <string name="wallet_filter_all">Vše</string> @@ -1957,6 +2220,8 @@ <string name="video_player_settings_action_pip_description">Přehrávat video v plovoucím okně (skryto, pokud není podporováno)</string> <string name="video_player_settings_action_cast">Odeslat do zařízení</string> <string name="video_player_settings_action_cast_description">Odeslat video do přijímače Chromecast ve vaší Wi-Fi (skryto pro místní soubory)</string> + <string name="video_player_settings_action_captions">Titulky</string> + <string name="video_player_settings_action_captions_description">Zapnout nebo vypnout titulky (skryto, pokud je video nemá)</string> <string name="audio_visualizer_settings_description">Vyberte animaci zobrazenou při přehrávání zvukových poznámek.</string> <string name="audio_visualizer_off">Vypnuto</string> <string name="audio_visualizer_bars">Spektrální pruhy</string> @@ -2644,6 +2909,7 @@ <string name="community_rules_violation_wot_gate_failed">Nesplňuješ požadavky sítě důvěry této komunity.</string> <string name="community_rules_violation_stale_rules">Nejnovější dokument s pravidly komunity byl odmítnut jako zastaralý.</string> <string name="marmot_groups_title">Skupiny Marmot</string> + <string name="cordn_groups_title">Skupiny Cordn</string> <string name="marmot_create_group">Vytvořit skupinu</string> <string name="marmot_create_group_title">Vytvořit skupinu Marmot</string> <string name="marmot_create_group_footer">Bude vytvořena nová MLS skupina. Členy můžete přidat poté.</string> @@ -2943,13 +3209,18 @@ <string name="app_notification_replies_channel_description">Upozorní vás, když někdo odpoví na váš příspěvek</string> <string name="app_notification_replies_channel_name">Odpovědi</string> <string name="app_notification_replies_summary">Nové odpovědi</string> + <string name="app_notification_reply_failed">Neodesláno</string> <string name="app_notification_reply_label">Odpovědět</string> + <string name="app_notification_reply_retry">Zkusit znovu</string> + <string name="app_notification_reply_sending">Odesílání…</string> + <string name="app_notification_reply_unconfirmed">Stále se odesílá…</string> <string name="app_notification_reposts_channel_description">Upozorní vás, když někdo přepošle váš příspěvek</string> <string name="app_notification_reposts_channel_message">%1$s přeposlal váš příspěvek</string> <string name="app_notification_reposts_channel_name">Přeposlání</string> <string name="app_notification_reposts_summary">Nová přeposlání</string> <string name="app_notification_scheduled_posts_channel_description">Oznámení, když je naplánovaný příspěvek publikován nebo selže při publikaci.</string> <string name="app_notification_scheduled_posts_channel_name">Naplánované příspěvky</string> + <string name="app_notification_shortcut_account_removed">Tento účet byl z Amethystu odebrán.</string> <string name="app_notification_zaps_channel_description">Upozorní vás, když vás někdo zapsne</string> <string name="app_notification_zaps_channel_message">%1$s satů</string> <string name="app_notification_zaps_channel_message_for">pro %1$s</string> @@ -3180,6 +3451,10 @@ <string name="call_unmute">Zrušit ztlumení</string> <string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration_with_user">Adresa URL zpětného volání nebyla nalezena v odpovědi %1$s</string> <string name="cancel">Zrušit</string> + <string name="captions_off">Vypnuto</string> + <string name="captions_turn_off">Vypnout titulky</string> + <string name="captions_turn_on">Zapnout titulky</string> + <string name="captions_unnamed_track">Titulky</string> <string name="cashu">Cashu Token</string> <string name="cashu_checking_mint">Kontroluji mint…</string> <string name="cashu_failed_redemption">Nepodařilo se použít Cashu</string> @@ -3693,10 +3968,12 @@ <string name="kind_adventure_prologue">Prolog dobrodružství</string> <string name="kind_adventure_reading">Čtení dobrodružství</string> <string name="kind_adventure_scene">Scéna dobrodružství</string> + <string name="kind_app_curation_set">Seznam aplikací</string> <string name="kind_app_recommendations">Doporučení aplikací</string> <string name="kind_appointment">Schůzka</string> <string name="kind_apps">Aplikace</string> <string name="kind_appt_rsvp">RSVP na schůzku</string> + <string name="kind_article_curation_set">Seznam článků</string> <string name="kind_audio_header">Audio hlavička</string> <string name="kind_audio_track">Audio stopa</string> <string name="kind_authored_podcasts">Vytvořené podcasty</string> @@ -3709,6 +3986,7 @@ <string name="kind_blossom_auth">Blossom ověření</string> <string name="kind_blossom_servers">Blossom servery</string> <string name="kind_bookmark_list">Seznam záložek</string> + <string name="kind_bookmark_set">Sada záložek</string> <string name="kind_broadcast_relays">Broadcast relaye</string> <string name="kind_calendar">Kalendář</string> <string name="kind_channel_definition">Definice kanálu</string> @@ -3746,6 +4024,7 @@ <string name="kind_emoji_packs">Balíčky emoji</string> <string name="kind_ephemeral_chat">Dočasný chat</string> <string name="kind_ephemeral_chatrooms">Dočasné chatovací místnosti</string> + <string name="kind_favorite_algo_feeds">Oblíbené kanály</string> <string name="kind_favorite_podcasts">Oblíbené podcasty</string> <string name="kind_favorite_relays">Oblíbené relaye</string> <string name="kind_file_headers">Hlavičky souborů</string> @@ -3754,28 +4033,36 @@ <string name="kind_follow_packs">Balíčky sledování</string> <string name="kind_geohash_follows">Sledování geohashe</string> <string name="kind_gift_wraps">Dárkové balíčky</string> + <string name="kind_git_authors">Git autoři</string> <string name="kind_git_issue">Git problém</string> <string name="kind_git_patch">Git záplata</string> <string name="kind_git_pr">Pull request</string> <string name="kind_git_pr_update">Aktualizace PR</string> <string name="kind_git_reply">Git odpověď</string> <string name="kind_git_repo">Git repozitář</string> + <string name="kind_git_repositories">Git repozitáře</string> <string name="kind_git_status_applied">Git stav: Aplikováno</string> <string name="kind_git_status_closed">Git stav: Uzavřeno</string> <string name="kind_git_status_draft">Git stav: Koncept</string> <string name="kind_git_status_open">Git stav: Otevřeno</string> + <string name="kind_good_wiki_authors">Důvěryhodní autoři wiki</string> + <string name="kind_good_wiki_relays">Důvěryhodná wiki relé</string> <string name="kind_hashtag_follows">Sledované hashtagy</string> <string name="kind_highlights">Zvýraznění</string> <string name="kind_http_auth">HTTP ověření</string> <string name="kind_index_relay_list">Seznam indexovacích relayů</string> + <string name="kind_interest_set">Zájmy</string> <string name="kind_live_chats">Živé chaty</string> <string name="kind_live_streams">Živé přenosy</string> + <string name="kind_media_follows">Sledování médií</string> + <string name="kind_media_starter_pack">Startovní balíček médií</string> <string name="kind_medical_data">Lékařská data</string> <string name="kind_meeting_room">Místnost schůzky</string> <string name="kind_meeting_space">Prostor schůzky</string> <string name="kind_music_playlist">Hudební playlist</string> <string name="kind_music_track">Hudební stopa</string> <string name="kind_mute_list">Seznam ztlumených</string> + <string name="kind_mute_set_for_kind">Ztlumeno pro druh %1$s</string> <string name="kind_named_bookmarks">Pojmenované záložky</string> <string name="kind_nests_servers">Servery Nests</string> <string name="kind_nip">NIP</string> @@ -3790,6 +4077,7 @@ <string name="kind_outbox_relays">Outbox relaye</string> <string name="kind_pay_to">Platba na</string> <string name="kind_people_lists">Seznamy lidí</string> + <string name="kind_picture_curation_set">Seznam obrázků</string> <string name="kind_pictures">Obrázky</string> <string name="kind_pins">Připnuté</string> <string name="kind_podcast_episode">Epizoda podcastu</string> @@ -3816,6 +4104,7 @@ <string name="kind_search_relays">Vyhledávací relaye</string> <string name="kind_shorts">Krátká videa</string> <string name="kind_shorts_repl">Krátká videa (odpověď)</string> + <string name="kind_simple_groups">Skupiny</string> <string name="kind_torrent_comments">Komentáře k torrentu</string> <string name="kind_torrents">Torrenty</string> <string name="kind_trusted_providers">Důvěryhodní poskytovatelé</string> @@ -3824,7 +4113,10 @@ <string name="kind_user_settings">Uživatelská nastavení</string> <string name="kind_user_status">Stav uživatele</string> <string name="kind_video">Video</string> + <string name="kind_video_collaboration">Spolupráce na videu</string> + <string name="kind_video_list">Seznam videí</string> <string name="kind_video_repl">Video (odpověď)</string> + <string name="kind_video_subtitles">Titulky videa</string> <string name="kind_voice_msg">Hlasová zpráva</string> <string name="kind_voice_reply">Hlasová odpověď</string> <string name="kind_wake">Probuzení</string> @@ -3865,6 +4157,8 @@ <string name="location_change_place">Změnit místo na mapě</string> <string name="location_channels">Kanály podle polohy</string> <string name="location_pick_on_map">Vyberte místo na mapě</string> + <string name="login_bunker_not_supported">Amethyst pro Android se nemůže přihlásit pomocí adresy bunker://. Tyto adresy rozdává, aby ho ostatní aplikace mohly žádat o podpis, ale přihlásit se s nimi nedokáže. Použijte externí podepisovací aplikaci nebo Amethyst Desktop.</string> + <string name="login_nostrconnect_not_supported">Toto je aplikace, která se chce připojit k vašemu podepisovateli, ne způsob přihlášení. Nejprve se přihlaste a pak ji otevřete z obrazovky podepisovatele NIP-46.</string> <string name="login_with_a_private_key_to_be_able_to_boost_posts">Pro zvýšení příspěvků se přihlaste s privátním klíčem</string> <string name="login_with_a_private_key_to_be_able_to_follow">Přihlaste se pomocí soukromého klíče, abyste mohli následovat</string> <string name="login_with_a_private_key_to_be_able_to_hide_word">Používáte veřejný klíč a veřejné klíče jsou pouze pro čtení. Přihlaste se pomocí soukromého klíče, abyste mohli skrýt slovo nebo větu</string> @@ -3965,6 +4259,7 @@ <string name="mute_notifications">Ztlumit oznámení</string> <string name="muted_button">Ztlumené. Klikněte pro odztlumení</string> <string name="my_blossom_data">Mé soubory Blossom</string> + <string name="my_fitness">Moje kondice</string> <string name="my_lists">Moje seznamy</string> <string name="name_is_required">Jméno je povinné</string> <string name="namecoin_response_time">%1$dms</string> @@ -4258,6 +4553,7 @@ </plurals> <string name="nip46_signer_remote_app">Aplikace vzdáleného podepisovatele</string> <string name="nip46_signer_title">Vzdálený podepisovatel</string> + <string name="nip51_list_has_private_members">Tento seznam má soukromé členy, které vidí jen jeho autor</string> <plurals name="nip82_assets_count"> <item quantity="one">%1$d majetek je zbalený</item> <item quantity="few">%1$d majetky jsou zbalený</item> @@ -4856,12 +5152,21 @@ <string name="route_calendars">Kalendáře</string> <string name="route_chess">Šachy</string> <string name="route_discover">Objevit</string> + <string name="route_edit_geocache">Upravit keš</string> + <string name="route_edit_geocache_hunt">Upravit výpravu</string> + <string name="route_geocache_detail">Keš</string> + <string name="route_geocache_hunt_detail">Výprava</string> + <string name="route_geocache_hunts">Výpravy</string> + <string name="route_geocaches">Keše</string> <string name="route_home">Domů</string> <string name="route_import_follows">Importovat sledované</string> + <string name="route_log_geocache_find">Zapsat nález</string> <string name="route_media">Média</string> <string name="route_messages">Zprávy</string> <string name="route_music_playlists">Playlisty</string> <string name="route_music_tracks">Hudba</string> + <string name="route_new_geocache">Ukrýt keš</string> + <string name="route_new_geocache_hunt">Nová výprava</string> <string name="route_notifications">Upozornění</string> <string name="route_podcast_episodes">Epizody</string> <string name="route_podcasts">Podcasty</string> @@ -5063,6 +5368,20 @@ <string name="vanish_request_sent">Žádost o zmizení odeslána</string> <string name="video_codec_h265_description">Lepší kvalita při menší velikosti souboru, ale ne všechna zařízení podporují přehrávání H.265.</string> <string name="video_codec_h265_label">Použít kodek H.265/HEVC</string> + <string name="video_collaboration_accepted_on">Přijal uvedení jako spolupracovník</string> + <string name="video_collaboration_declined_on">Odmítl uvedení jako spolupracovník</string> + <string name="video_credit_accepted">Uvedení přijato</string> + <string name="video_credit_audio_from">Zvuk od</string> + <string name="video_credit_featuring">Účinkuje</string> + <string name="video_credit_inspired_by">Inspirováno</string> + <string name="video_credit_references">Odkazuje na</string> + <string name="video_curation_set_private_items">Soukromý seznam</string> + <plurals name="video_curation_set_video_count"> + <item quantity="one">%1$d video</item> + <item quantity="few">%1$d videa</item> + <item quantity="many">%1$d videa</item> + <item quantity="other">%1$d videí</item> + </plurals> <string name="video_download_has_started_toast">Stahování videa bylo zahájeno…</string> <string name="video_player_settings">Tlačítka přehrávače videa</string> <string name="video_saved_to_the_gallery">Video uloženo do video galerie telefonu</string> @@ -5128,169 +5447,20 @@ <string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">Služba blesku příjemce v %1$s není k dispozici. Byla vypočtena z adresy blesku "%2$s". Chyba: %3$s. Zkontrolujte, zda je server aktivní a zda je adresa blesku správná</string> <string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">Nelze vytvořit bleskovou fakturu z %1$s: Prvek pr nebyl nalezen ve výsledném JSON.</string> <string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">Nelze vytvořit bleskovou fakturu. Zpráva od %1$s: %2$s</string> + <string name="marmot_invite_device_success">Toto zařízení nyní přijímá vaše pozvánky Marmot.</string> + <string name="marmot_invite_device_failure">Zveřejnění KeyPackage selhalo: %1$s</string> + <string name="marmot_invite_device_rejected">Žádné relé KeyPackage nepřijalo. Zkontrolujte nastavení relé a zkuste to znovu.</string> + <string name="chat_delivery_sending">Odesílání…</string> + <string name="chat_delivery_failed">Nepodařilo se odeslat. Klepnutím to zkusíte znovu.</string> + <plurals name="marmot_unread_messages"> + <item quantity="one">%1$d nová zpráva</item> + <item quantity="few">%1$d nové zprávy</item> + <item quantity="many">%1$d nové zprávy</item> + <item quantity="other">%1$d nových zpráv</item> + </plurals> <!-- cordn: groups delivered by an MCP coordinator rather than by relays. The exposure strings state what the coordinator operator learns; see spec/00.md §8 and quartz/plans/2026-09-17-cordn-interop.md. --> - <!-- cordn entries in the settings catalog and the Messages chat-type - list. Here rather than in amethyst/res because both screens read - them through Res. --> - <!-- cordn strings on screens main migrated to Res: the shared inbox row - and the new-conversation picker. --> - <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a - one-shot handoff rather than continuous sync. The exposure strings say - what leaves the device; the fork strings say why the old phone stops. --> - <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> - <string name="geocache_unnamed">Keš</string> - <string name="geocache_type_traditional">Tradiční</string> - <string name="geocache_type_multi">Multi-keš</string> - <string name="geocache_type_mystery">Mystery</string> - <string name="geocache_size_micro">Mikro</string> - <string name="geocache_size_small">Malá</string> - <string name="geocache_size_regular">Normální</string> - <string name="geocache_size_large">Velká</string> - <string name="geocache_size_other">Jiná velikost</string> - <string name="geocache_difficulty_short">D%1$d</string> - <string name="geocache_terrain_short">T%1$d</string> - <string name="geocache_first_to_find">První nálezce</string> - <string name="geocache_is_art">Umělecké dílo</string> - <string name="geocache_mission">Key Quest</string> - <string name="geocache_needs_verification">Ověřené nálezy</string> - <string name="geocache_claimed">Obsazeno</string> - <string name="geocache_archived">Archivováno</string> - <string name="geocache_hint_tap_to_reveal">Nevíte si rady? Klepnutím zobrazíte nápovědu</string> - <string name="geocache_hint_other_reading">Nečitelné? Klepnutím zobrazíte druhé čtení</string> - <string name="geocache_nearby">Poblíž</string> - <string name="geocache_found_it">Nalezeno!</string> - <string name="geocache_verified_find">Ověřený nález</string> - <string name="geocache_invalid_proof">Důkaz nesouhlasí</string> - <string name="geocache_loading">Načítání keše…</string> - <string name="geocache_hidden_by">Ukryl %1$s</string> - <string name="geocache_navigate">Navigovat</string> - <string name="geocache_copy_coordinates">Kopírovat souřadnice</string> - <string name="geocache_coordinates_copied">Souřadnice zkopírovány</string> - <string name="geocache_photos_section">Fotky</string> - <string name="geocache_mission_section">Mise Key Quest</string> - <string name="geocache_logs_section">Logy</string> - <string name="geocache_no_logs">Tuto keš zatím nikdo nezalogoval. Buďte první.</string> - <string name="geocache_i_found_it">Nalezeno</string> - <string name="geocache_didnt_find_it">Nenalezeno</string> - <string name="geocache_add_note">Přidat poznámku</string> - <string name="geocache_needs_maintenance">Nahlásit potřebu údržby</string> - <string name="geocache_add_to_hunt">Přidat do výpravy</string> - <string name="geocache_hunt_new">Nová výprava…</string> - <string name="geocache_hunt_none_yet">Zatím jste nevytvořili žádnou výpravu.</string> - <string name="geocache_hunt_already_on">Už je v této výpravě</string> - <plurals name="geocache_caches_here"> - <item quantity="one">%1$d keš zde</item> - <item quantity="few">%1$d keše zde</item> - <item quantity="many">%1$d keše zde</item> - <item quantity="other">%1$d keší zde</item> - </plurals> - <string name="geocache_photo_remove">Odebrat tuto fotku</string> - <string name="geocache_archived_notice">Tato keš byla archivována. Už není ve hře.</string> - <string name="geocache_ftf_won_by">Prvním nálezcem se stal %1$s</string> - <string name="geocache_ftf_available">Místo prvního nálezce je stále volné</string> - <string name="geocache_owner_actions">Vlastník</string> - <string name="geocache_owner_edit">Upravit tuto keš</string> - <string name="geocache_owner_archive">Archivovat tuto keš</string> - <string name="geocache_owner_archive_confirm">Archivovat. Nálezci uvidí, že už není ve hře, a listing zůstane čitelný.</string> - <string name="geocache_owner_lock_ftf">Potvrdit prvního nálezce</string> - <string name="geocache_owner_lock_ftf_explain">Zveřejní tag F se jménem %1$s. Je to trvalé.</string> - <string name="geocache_owner_no_finders">Zatím žádné ověřené nálezy, takže není koho potvrdit.</string> - <string name="geocache_owner_show_qr">Zobrazit ověřovací kód</string> - <string name="geocache_owner_qr_warning">Vytiskněte ho a vložte do keše. Amethyst ho neukládá — pokud ho ztratíte, nálezci už nebudou moci prokázat, že tam byli.</string> - <string name="geocache_log_type_dnf">Nenalezeno</string> - <string name="geocache_log_type_note">Poznámka</string> - <string name="geocache_log_type_maintenance">Potřebuje údržbu</string> - <string name="geocache_log_title">Zapsat nález</string> - <string name="geocache_log_cache_label">Keš</string> - <string name="geocache_log_your_log">Váš log</string> - <string name="geocache_log_placeholder">Jak to šlo?</string> - <string name="geocache_log_add_photo">Přidat fotku</string> - <string name="geocache_log_mission_answer">Vaše odpověď Key Quest</string> - <string name="geocache_log_proof_section">Důkaz přítomnosti</string> - <string name="geocache_log_scan_code">Naskenujte kód v keši</string> - <string name="geocache_log_scan_again">Naskenovat jiný kód</string> - <string name="geocache_log_proof_verified">Ověřeno — byli jste u keše</string> - <string name="geocache_log_proof_explain">Jednou podepsáno kódem z krabičky. Klíč se nikdy neukládá a nikdy neopustí tuto obrazovku.</string> - <string name="geocache_log_proof_bad_code">To není ověřovací kód.</string> - <string name="geocache_log_proof_wrong_cache">Tento kód patří jiné keši.</string> - <string name="geocache_log_proof_optional">Tato keš nevyžaduje důkaz, takže ji můžete rovnou zalogovat.</string> - <string name="geocache_log_no_location_attached">K tomuto logu není připojena vaše vlastní poloha.</string> - <string name="geocache_log_post">Odeslat</string> - <string name="geocache_log_comment_title">Přidat log</string> - <string name="geocache_tab_nearby">Poblíž</string> - <string name="geocache_tab_map">Mapa</string> - <string name="geocache_tab_hunts">Výpravy</string> - <string name="geocache_tab_finds">Nálezy</string> - <string name="geocache_tab_mine">Moje</string> - <string name="geocache_filter_unfound">Nenalezené</string> - <string name="geocache_filter_difficulty">D ≤ %1$d</string> - <string name="geocache_filter_terrain">T ≤ %1$d</string> - <string name="geocache_filter_has_prize">S cenou</string> - <string name="geocache_filter_verified_only">Vyžaduje důkaz</string> - <string name="geocache_use_my_location">Použít mou polohu</string> - <string name="geocache_sorted_by_distance">Seřazeno podle vzdálenosti</string> - <string name="geocache_sorted_by_recent">Nejnovější první</string> - <string name="geocache_hide_a_cache">Ukrýt keš</string> - <string name="geocache_empty_nearby">Zatím tu nejsou žádné keše. Zkuste širší seznam sledovaných, nebo ukryjte první.</string> - <string name="geocache_empty_finds">Zatím jste nezapsali žádný nález.</string> - <string name="geocache_empty_mine">Zatím jste neukryli žádnou keš.</string> - <string name="geocache_empty_hunts">Zatím žádné výpravy.</string> - <string name="geocache_stat_finds">Nálezy</string> - <string name="geocache_stat_ftf">První nálezy</string> - <string name="geocache_stat_verified">Ověřené</string> - <string name="geocache_stat_hidden">Ukryté</string> - <string name="geocache_map_recenter">Vycentrovat na mě</string> - <string name="geocache_map_long_press">Dlouhým stiskem na mapě tam ukryjete keš</string> - <string name="geocache_map_open">Otevřít</string> - <string name="geocache_new_section_where">Kde</string> - <string name="geocache_new_section_what">Co</string> - <string name="geocache_new_section_extras">Doplňky</string> - <string name="geocache_new_section_proof">Důkaz</string> - <string name="geocache_new_section_relays">Relaye</string> - <string name="geocache_new_location_warning">Tímto zveřejníte veřejnou polohu s přesností asi na 5 metrů. Uvidí ji kdokoli na Nostru a zveřejněnou událost nelze vzít zpět.</string> - <string name="geocache_new_pick_location">Vyberte místo</string> - <string name="geocache_new_ladder">Zveřejní %1$s a k tomu hrubší buňky až po 3 znaky pro vyhledávání v okolí.</string> - <string name="geocache_new_name">Název</string> - <string name="geocache_new_description">Popis</string> - <string name="geocache_new_type">Typ</string> - <string name="geocache_new_size">Velikost</string> - <string name="geocache_new_difficulty">Obtížnost</string> - <string name="geocache_new_terrain">Terén</string> - <string name="geocache_new_hint">Nápověda</string> - <string name="geocache_new_hint_preview">Nálezci uvidí</string> - <string name="geocache_new_hint_scramble">Zašifrovat nápovědu (ROT13)</string> - <string name="geocache_new_mission">Mise Key Quest</string> - <string name="geocache_new_modifiers">Modifikátory</string> - <string name="geocache_new_require_proof">Vyžadovat důkaz přítomnosti</string> - <string name="geocache_new_proof_ready">Pro tuto keš byl vygenerován ověřovací klíč.</string> - <string name="geocache_new_log_relays">Kam mají nálezci logovat</string> - <string name="geocache_new_publish">Zveřejnit</string> - <string name="geocache_new_needs_name">Pojmenujte keš.</string> - <string name="geocache_new_needs_location">Vyberte, kde je keš ukrytá.</string> - <string name="geocache_hunt_progress">Nalezeno %1$d z %2$d</string> - <string name="geocache_hunt_start">Zahájit výpravu</string> - <plurals name="geocache_hunt_caches"> - <item quantity="one">%1$d keš</item> - <item quantity="few">%1$d keše</item> - <item quantity="many">%1$d keše</item> - <item quantity="other">%1$d keší</item> - </plurals> - <string name="geocache_hunt_title">Název</string> - <string name="geocache_hunt_description">Popis</string> - <string name="geocache_hunt_banner">Obrázek banneru</string> - <string name="geocache_hunt_pick_caches">Keše v pořadí</string> - <string name="geocache_hunt_add_cache">Přidat keš</string> - <string name="geocache_hunt_move_up">Posunout nahoru</string> - <string name="geocache_hunt_move_down">Posunout dolů</string> - <string name="geocache_hunt_remove">Odebrat</string> - <string name="geocache_hunt_theme">Motiv stránky</string> - <string name="geocache_hunt_map_style">Styl mapy</string> - <string name="geocache_hunt_needs_title">Pojmenujte výpravu.</string> - <string name="geocache_hunt_needs_caches">Přidejte alespoň jednu keš.</string> - <string name="geocache_publish_failed">Zveřejnění se nezdařilo. Zkontrolujte připojení k relayům a zkuste to znovu.</string> - <string name="cordn_groups_title">Skupiny Cordn</string> <string name="cordn_badge_coordinator">Skupina s koordinátorem</string> <string name="cordn_badge_coordinator_desc">Doručováno koordinátorem, ne relayi</string> <string name="cordn_exposure_title">Co tento koordinátor vidí</string> @@ -5330,6 +5500,9 @@ <string name="cordn_link_relays">Dostupné přes</string> <string name="cordn_link_no_coordinator">Tento odkaz neuvádí žádného koordinátora, takže ho samotný nelze použít. Zeptejte se odesílatele, který koordinátor skupinu přenáší.</string> <string name="cordn_link_not_joinable">Prohlédnutím odkazu se k ničemu nepřipojíte a to, že ho máte, z vás nedělá člena.</string> + <!-- cordn entries in the settings catalog and the Messages chat-type + list. Here rather than in amethyst/res because both screens read + them through Res. --> <string name="cordn_link_title">Odkaz na skupinu Cordn</string> <string name="cordn_link_search_keywords">cordn koordinátor skupina odkaz pozvánka mls chat metadata soukromí odhalení coordinator group link invite privacy exposure</string> <string name="cordn_coordinators_title">Koordinátoři Cordn</string> @@ -5338,6 +5511,10 @@ <string name="cordn_keypackages_search_keywords">cordn balíček klíčů zveřejnit pozvánka záložní koordinátor mls key package publish invite last resort coordinator</string> <string name="cordn_backup_title">Záloha Cordn</string> <string name="cordn_backup_search_keywords">cordn záloha obnovení export import heslo skupiny obnova backup restore passphrase groups recovery</string> + <string name="chat_type_cordn_title">Skupiny Cordn</string> + <string name="chat_type_cordn_desc">End-to-end šifrované skupiny, jejichž pořadí zpráv určuje vámi zvolený koordinátor, nikoli relé.</string> + <!-- cordn strings on screens main migrated to Res: the shared inbox row + and the new-conversation picker. --> <string name="cordn_group_untitled">Skupina %1$s</string> <string name="cordn_group_no_messages_yet">Zatím žádné zprávy</string> <string name="cordn_group_via_coordinator">Skupina Cordn přenášená přes %1$s</string> @@ -5346,8 +5523,19 @@ <string name="cordn_preview_photo">Fotka</string> <string name="cordn_preview_video">Video</string> <string name="cordn_preview_file">Soubor</string> + <string name="new_conversation_cordn_title">Skupina Cordn</string> + <string name="new_conversation_cordn_tagline">Šifrovaný skupinový chat řazený koordinátorem, kterého si vyberete.</string> + <string name="new_conversation_cordn_chip">Koordinovaná</string> + <string name="new_conversation_cordn_best">Nejlepší pro týmy, které chtějí jedno spolehlivé pořadí konverzace.</string> + <string name="new_conversation_cordn_cta">Vytvořit skupinu Cordn</string> + <string name="new_conversation_cordn_pro_1">End-to-end šifrování pomocí MLS — koordinátor nikdy nepřečte žádnou zprávu.</string> + <string name="new_conversation_cordn_pro_2">Jedno dohodnuté pořadí pro všechny, takže historii nemohou přeházet špatně nastavené hodiny.</string> + <string name="new_conversation_cordn_con_1">Koordinátor zjistí, kdo je ve skupině a kdy spolu mluvíte, i když nemůže číst, co píšete.</string> <string name="cordn_invitations_entry">Máte pozvánku do skupiny cordn?</string> <string name="cordn_invitations_entry_action">Zobrazit pozvánky</string> + <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a + one-shot handoff rather than continuous sync. The exposure strings say + what leaves the device; the fork strings say why the old phone stops. --> <string name="cordn_hub_title">Cordn</string> <string name="cordn_hub_explainer">Skupiny doručované koordinátorem, kterého si sami zvolíte, místo relayů.</string> <string name="cordn_hub_link">Prohlédnout odkaz na skupinu</string> @@ -5399,489 +5587,6 @@ <string name="cordn_migrate_no_server">Nejprve nastavte mediální server — zašifrované dokumenty musí někde počkat, než si je druhý telefon stáhne.</string> <string name="cordn_migrate_exposure_title">Co opouští toto zařízení</string> <string name="cordn_migrate_exposure_body">Stav vašich skupin se zašifruje a nahraje na váš mediální server, aby si ho druhý telefon mohl stáhnout. Server ho nedokáže přečíst, vidí jen velikost a čas. Pokud nechcete nic zanechat, soubory potom smažte.</string> - <string name="cordn_action_delete">Smazat</string> - <string name="cordn_action_edit">Upravit</string> - <string name="cordn_action_editing">Úprava vaší zprávy</string> - <string name="cordn_action_go_to_message">Přejít na zprávu</string> - <string name="cordn_action_pin">Připnout</string> - <string name="cordn_action_reply">Odpovědět</string> - <string name="cordn_action_unpin">Odepnout</string> - <string name="cordn_admin_action_failed">Tuto změnu nebylo možné provést.</string> - <string name="cordn_admin_coordinator_silent">Koordinátor neodpověděl, takže se nic nezměnilo. Zkuste to za chvíli znovu.</string> - <string name="cordn_admin_no_key_package">%1$s na tomto koordinátoru nezveřejnil balíček klíčů, takže ho zatím nelze přidat. Požádejte ho, aby na tomto koordinátoru nejprve otevřel cordn.</string> - <string name="cordn_admin_no_self_remove">Ze skupiny cordn nemůžete odebrat sami sebe.</string> - <string name="cordn_admin_no_welcome">Pro %1$s se nepodařilo zajistit přístup do skupiny, takže přidání bylo zrušeno.</string> - <string name="cordn_admin_not_a_member">%1$s není v této skupině.</string> - <string name="cordn_admin_someone">Tato osoba</string> - <string name="cordn_admin_wrong_key_package">Tento koordinátor poskytl balíček klíčů, který patří někomu jinému, takže %1$s nebyl přidán. Nic se nezměnilo.</string> - <string name="cordn_backup_contents_body">Šifrovací klíče vašich skupin, vaše balíčky klíčů a všechny zprávy v těchto skupinách. Kdokoli soubor otevře, může konverzace číst. Jakmile soubor opustí Amethyst, chrání ho jen heslo.</string> - <string name="cordn_backup_contents_title">Co soubor obsahuje</string> - <string name="cordn_backup_explainer">Váš nsec skupiny cordn neobnoví. Skupina cordn existuje jako šifrovací stav na tomto zařízení a jako seřazený proud zpráv na koordinátoru, který ho nedokáže přečíst — přijdete-li o zařízení bez zálohy, skupina je pryč, včetně všeho, co v ní už zaznělo.</string> - <string name="cordn_backup_export">Exportovat</string> - <string name="cordn_backup_exported">Záloha uložena.</string> - <string name="cordn_backup_failed">Nepovedlo se. Zkontrolujte heslo a soubor.</string> - <string name="cordn_backup_passphrase">Heslo</string> - <string name="cordn_backup_restore">Obnovit</string> - <string name="cordn_backup_restore_body">Obnovení nahradí skupiny cordn na tomto zařízení těmi ze souboru. Je určeno pro telefon, který převzal roli jiného — ne k používání stejných skupin na dvou zařízeních zároveň.</string> - <string name="cordn_backup_restore_confirm_body">Všechny skupiny cordn, které jsou nyní na tomto zařízení, budou odebrány a nahrazeny těmi ze souboru.\n\nNeobnovujte zálohu na druhý telefon, dokud se ten první stále používá. Oba by měly stejné klíče skupin, a jakmile oba odešlou zprávu, skupina se rozbije pro všechny — potichu a nevratně.</string> - <string name="cordn_backup_restore_confirm_title">Nahradit skupiny cordn na tomto zařízení?</string> - <string name="cordn_backup_restore_title">Obnovit</string> - <string name="cordn_backup_restored">Obnoveno.</string> - <string name="cordn_chat_empty_description">Vše, co sem pošlete, je zašifrováno pro tuto skupinu. Koordinátor to předává dál, aniž by to mohl přečíst.</string> - <string name="cordn_chat_empty_title">Zatím žádné zprávy</string> - <string name="cordn_chat_unread_divider">Nové zprávy</string> - <string name="cordn_chat_yesterday">Včera</string> - <string name="cordn_composer_hint">Zpráva</string> - <string name="cordn_coordinators_add_action">Přidat</string> - <string name="cordn_coordinators_add_failed">Tohoto koordinátora se nepodařilo otevřít.</string> - <string name="cordn_coordinators_copy_key">Kopírovat npub koordinátora</string> - <string name="cordn_coordinators_discover_action">Hledat koordinátory</string> - <string name="cordn_coordinators_discover_add">Přidat</string> - <string name="cordn_coordinators_discover_explainer">Čte oznámení, která už vaše relaye přenášejí. Žádný koordinátor není kontaktován, takže se žádný z nich nedozví, že jste hledali.</string> - <string name="cordn_coordinators_discover_failed">Oznámení se nepodařilo načíst</string> - <string name="cordn_coordinators_discover_none">Na vašich relayích se nikdo neohlašuje.</string> - <string name="cordn_coordinators_discover_seen">Ohlášeno před %1$s</string> - <string name="cordn_coordinators_discover_seen_on">Naposledy ohlášeno %1$s</string> - <plurals name="cordn_coordinators_discover_unheard"> - <item quantity="one">Nic nenalezeno a %1$d z vašich relayů neodpověděl.</item> - <item quantity="few">Nic nenalezeno a %1$d z vašich relayů neodpověděly.</item> - <item quantity="many">Nic nenalezeno a %1$d z vašich relayů neodpovědělo.</item> - <item quantity="other">Nic nenalezeno a %1$d z vašich relayů neodpovědělo.</item> - </plurals> - <string name="cordn_coordinators_explainer">Koordinátor určuje pořadí zpráv jedné skupiny a nic jiného to dělat nemůže. Jeho ztrátou se ztratí i pořadí a druhý koordinátor ten první nezrcadlí — proto jde o konkrétní servery, které si zvolíte, ne o zaměnitelné relaye.</string> - <plurals name="cordn_coordinators_health_down"> - <item quantity="one">Neodpovídá — %1$d požadavek za sebou selhal. Skupiny na něm se nesynchronizují.</item> - <item quantity="few">Neodpovídá — %1$d požadavky za sebou selhaly. Skupiny na něm se nesynchronizují.</item> - <item quantity="many">Neodpovídá — %1$d požadavku za sebou selhalo. Skupiny na něm se nesynchronizují.</item> - <item quantity="other">Neodpovídá — %1$d požadavků za sebou selhalo. Skupiny na něm se nesynchronizují.</item> - </plurals> - <string name="cordn_coordinators_health_ok">Odpovídá.</string> - <string name="cordn_coordinators_health_retrying">Požadavek selhal, zkouší se znovu.</string> - <string name="cordn_coordinators_health_unknown">Zatím nebyl o nic požádán.</string> - <string name="cordn_coordinators_hide_older">Skrýt ty, které se přestaly ohlašovat</string> - <string name="cordn_coordinators_identify">Zjistit, kdo to je</string> - <string name="cordn_coordinators_key">Klíč koordinátora</string> - <string name="cordn_coordinators_label">Název (váš vlastní, volitelný)</string> - <string name="cordn_coordinators_label_note">Tento název je váš a zůstává na tomto zařízení. Koordinátor nemůže název prokázat, proto se ho na něj nikdy neptáme.</string> - <string name="cordn_coordinators_more">Další akce</string> - <string name="cordn_coordinators_none">Zatím žádní koordinátoři.</string> - <string name="cordn_coordinators_purge">Vymazat</string> - <string name="cordn_coordinators_purge_body">Odebráním zůstanou skupiny na tomto zařízení, takže po opětovném přidání koordinátora se vrátí s ním. Vymazáním se smažou: klíče skupin, pozice čtení i vaše balíčky klíčů zmizí a do těchto skupin se lze vrátit jen s novou pozvánkou.\n\nKoordinátor se o tom nedozví a ponechá si vše, co už měl.</string> - <string name="cordn_coordinators_purge_title">Vymazat tohoto koordinátora?</string> - <string name="cordn_coordinators_relays">%1$s</string> - <string name="cordn_coordinators_relays_label">Odpovídá na</string> - <string name="cordn_coordinators_relays_more">%1$s +%2$d další</string> - <string name="cordn_coordinators_remove">Odebrat</string> - <string name="cordn_coordinators_rename">Přejmenovat</string> - <string name="cordn_coordinators_server">Tvrdí, že je: %1$s</string> - <string name="cordn_coordinators_server_claim">Název a verzi si koordinátor zvolil sám. Identifikuje ho jen jeho veřejný klíč.</string> - <string name="cordn_coordinators_server_silent">Odpověděl, ale nic neuvedl.</string> - <string name="cordn_coordinators_show_all">Zobrazit vše (%1$d)</string> - <string name="cordn_coordinators_show_fewer">Zobrazit méně</string> - <plurals name="cordn_coordinators_show_older"> - <item quantity="one">Zobrazit %1$d, který se přestal ohlašovat</item> - <item quantity="few">Zobrazit %1$d, které se přestaly ohlašovat</item> - <item quantity="many">Zobrazit %1$d, které se přestaly ohlašovat</item> - <item quantity="other">Zobrazit %1$d, které se přestaly ohlašovat</item> - </plurals> - <string name="cordn_coordinators_stale_note">Tito koordinátoři se neohlásili déle než měsíc. Vytvoření skupiny na takovém, který už zanikl, selže.</string> - <string name="cordn_create_action">Vytvořit skupinu</string> - <plurals name="cordn_create_action_invite"> - <item quantity="one">Vytvořit a pozvat %1$d osobu</item> - <item quantity="few">Vytvořit a pozvat %1$d osoby</item> - <item quantity="many">Vytvořit a pozvat %1$d osoby</item> - <item quantity="other">Vytvořit a pozvat %1$d osob</item> - </plurals> - <string name="cordn_create_add_member">Přidat někoho</string> - <string name="cordn_create_admin">Administrátor</string> - <plurals name="cordn_create_admin_count"> - <item quantity="one">%1$d osoba může přidávat a odebírat</item> - <item quantity="few">%1$d osoby mohou přidávat a odebírat</item> - <item quantity="many">%1$d osoby může přidávat a odebírat</item> - <item quantity="other">%1$d osob může přidávat a odebírat</item> - </plurals> - <string name="cordn_create_admin_only_me">Tuto skupinu spravuji jen já</string> - <string name="cordn_create_admin_only_me_on">Jen vy můžete zvát, odebírat členy nebo měnit údaje skupiny. Později to můžete předat někomu jinému.</string> - <string name="cordn_create_coordinator">Koordinátor</string> - <string name="cordn_create_coordinator_change">Změnit</string> - <string name="cordn_create_coordinator_discover">Najít koordinátory</string> - <string name="cordn_create_coordinator_hide">Hotovo</string> - <string name="cordn_create_coordinator_new">Jiný koordinátor</string> - <string name="cordn_create_coordinator_pubkey">Veřejný klíč koordinátora (hex nebo npub)</string> - <string name="cordn_create_coordinator_relays">Relaye, na kterých odpovídá, jeden na řádek</string> - <plurals name="cordn_create_coverage_all"> - <item quantity="one">Zastihne vaši %1$d osobu</item> - <item quantity="few">Zastihne všechny %1$d vaše osoby</item> - <item quantity="many">Zastihne %1$d vaší osoby</item> - <item quantity="other">Zastihne všech %1$d vašich osob</item> - </plurals> - <string name="cordn_create_coverage_partial">Zastihne %1$d z %2$d — ostatní by zůstali mimo</string> - <string name="cordn_create_coverage_unknown">Neověřeno — výběrem se to zjistí</string> - <string name="cordn_create_description">Popis (volitelné)</string> - <string name="cordn_create_egalitarian">Všichni mohou přidávat a odebírat</string> - <string name="cordn_create_egalitarian_note">Každý ve skupině může zvát, odebírat členy a měnit její údaje. Kterýkoli člen může později jmenovat administrátory, čímž tento režim skončí.</string> - <string name="cordn_create_explainer">Pořadí zpráv ve skupině cordn určuje jeden koordinátor, kterého si zvolíte. Skupina se vytváří na tomto zařízení — koordinátor se o její existenci dozví, až někoho pozvete nebo pošlete zprávu.</string> - <string name="cordn_create_failed">Skupinu se nepodařilo vytvořit.</string> - <string name="cordn_create_name">Název skupiny</string> - <string name="cordn_create_no_coordinator">Není zvolen žádný koordinátor</string> - <string name="cordn_create_outcome_explainer">Každá pozvánka je zpráva Welcome ponechaná na koordinátoru, takže mohou selhat i jednotlivě. Nikdo nedostal oznámení: pozvánku uvidí, až příště otevřou svou aplikaci.</string> - <string name="cordn_create_outcome_failed">Koordinátor pozvánku odmítl</string> - <string name="cordn_create_outcome_link">Poslat odkaz</string> - <string name="cordn_create_outcome_no_welcome">Přidán do skupiny, ale pozvánka ho nezastihla — zkuste to znovu z obrazovky skupiny</string> - <string name="cordn_create_outcome_open">Otevřít chat</string> - <string name="cordn_create_outcome_skipped">Zde není žádný klíč, takže nešlo ponechat zprávu Welcome</string> - <string name="cordn_create_outcome_title">Skupina existuje</string> - <string name="cordn_create_outcome_waiting">Čeká se na ně</string> - <string name="cordn_create_people_done">Hotovo</string> - <string name="cordn_create_people_empty">Zatím nikdo nepřidán. Skupinu lze vytvořit prázdnou a pozvánky poslat později, ale právě uvedením lidí teď zjistíte, který koordinátor je zastihne.</string> - <string name="cordn_create_people_none">Zatím nikdo — klepnutím přidáte lidi</string> - <string name="cordn_create_people_note">Uvedením lidí teď zjistíme, který koordinátor je zastihne. Každý z nich dostane zprávu Welcome ponechanou na tomto koordinátoru a vyzvedne si ji, až příště otevře svou aplikaci.</string> - <plurals name="cordn_create_reach_count"> - <item quantity="one">Dostupný na %1$d koordinátoru</item> - <item quantity="few">Dostupný na %1$d koordinátorech</item> - <item quantity="many">Dostupný na %1$d koordinátoru</item> - <item quantity="other">Dostupný na %1$d koordinátorech</item> - </plurals> - <string name="cordn_create_reach_none">Žádný klíč na žádném z vašich koordinátorů</string> - <string name="cordn_create_reach_unknown">Zatím neověřeno</string> - <string name="cordn_create_step_admins">Kdo může přidávat a odebírat</string> - <string name="cordn_create_step_name">Pojmenování</string> - <string name="cordn_create_step_people">Kdo v ní bude</string> - <string name="cordn_create_step_where">Kde poběží</string> - <string name="cordn_create_title">Nová skupina Cordn</string> - <plurals name="cordn_create_unreachable_count"> - <item quantity="one">%1$d osobu nezastihne žádný z vašich koordinátorů</item> - <item quantity="few">%1$d osoby nezastihne žádný z vašich koordinátorů</item> - <item quantity="many">%1$d osoby nezastihne žádný z vašich koordinátorů</item> - <item quantity="other">%1$d osob nezastihne žádný z vašich koordinátorů</item> - </plurals> - <string name="cordn_create_you">Vy, vždy administrátor</string> - <string name="cordn_delete_confirm_body">Všichni ve skupině uvidí tuto zprávu jako staženou a Amethyst přestane zobrazovat její text. Koordinátor si stále uchovává zašifrovanou kopii, v jaké byla odeslána, a kdo ji už četl, ten ji už četl.</string> - <string name="cordn_delete_confirm_title">Stáhnout zprávu</string> - <string name="cordn_delivery_accepted">Přijato koordinátorem</string> - <string name="cordn_exposure_close">Rozumím</string> - <string name="cordn_exposure_open">Co tento koordinátor vidí</string> - <string name="cordn_group_info">Informace o skupině</string> - <string name="cordn_group_unavailable">Tato skupina není dostupná. Její koordinátor mohl být odebrán.</string> - <string name="cordn_group_unavailable_detail">Přidejte koordinátora v Nastavení, abyste mohli začít používat skupiny cordn.</string> - <plurals name="cordn_groups_count"> - <item quantity="one">%1$d skupina</item> - <item quantity="few">%1$d skupiny</item> - <item quantity="many">%1$d skupiny</item> - <item quantity="other">%1$d skupin</item> - </plurals> - <string name="cordn_groups_down_note">Z těchto skupin nic nepřichází. Amethyst to stále zkouší znovu.</string> - <string name="cordn_groups_manage">Koordinátoři</string> - <string name="cordn_groups_none">Zatím žádné skupiny cordn</string> - <string name="cordn_groups_none_detail">Skupina cordn žije na jednom koordinátoru, který spravuje její členství a předává zprávy, aniž by kdy viděl, co kdo píše.</string> - <string name="cordn_groups_start">Založit skupinu</string> - <string name="cordn_info_add_member">Přidat někoho</string> - <string name="cordn_info_add_member_none">Pod tímto jménem nikdo nenalezen.</string> - <string name="cordn_info_add_member_note">Přidat lze jen ty, kdo na tomto koordinátoru zveřejnili balíček klíčů.</string> - <string name="cordn_info_add_member_placeholder">Jméno, npub nebo jmeno@domena</string> - <string name="cordn_info_admin_badge">Admin</string> - <plurals name="cordn_info_admins_kept"> - <item quantity="one">%1$d administrátor skupiny zůstává beze změny.</item> - <item quantity="few">%1$d administrátoři skupiny zůstávají beze změny.</item> - <item quantity="many">%1$d administrátora skupiny zůstává beze změny.</item> - <item quantity="other">%1$d administrátorů skupiny zůstává beze změny.</item> - </plurals> - <string name="cordn_info_coordinator">Koordinátor</string> - <string name="cordn_info_coordinator_key">Klíč koordinátora</string> - <string name="cordn_info_coordinator_nprofile">Odkaz na koordinátora</string> - <string name="cordn_info_copy_nprofile">Kopírovat nprofile koordinátora</string> - <string name="cordn_info_edit_details">Upravit údaje</string> - <string name="cordn_info_egalitarian">Tato skupina nemá administrátory: každý může přidávat, odebírat a přejmenovávat, a to natrvalo.</string> - <string name="cordn_info_epoch">Epocha</string> - <string name="cordn_info_gid">ID skupiny</string> - <string name="cordn_info_has_key_package">Lze přidat na tomto koordinátoru</string> - <string name="cordn_info_members">Členové</string> - <string name="cordn_info_remove_confirm_body">%1$s odsud přestane dostávat zprávy. Co už přečetl, mu zůstane, a nelze to vrátit zpět — musel by být pozván znovu.</string> - <string name="cordn_info_remove_confirm_title">Odebrat ze skupiny?</string> - <string name="cordn_info_remove_member">Odebrat</string> - <string name="cordn_info_save">Uložit</string> - <string name="cordn_info_technical">Technické podrobnosti</string> - <string name="cordn_invitations_accept">Připojit se</string> - <string name="cordn_invitations_decline">Odmítnout</string> - <string name="cordn_invitations_decline_warning">Odmítnutí je trvalé. Pro návrat budete potřebovat novou pozvánku.</string> - <string name="cordn_invitations_explainer">Kontroluje se jen při otevření této obrazovky a nikdy jindy — dotaz na koordinátora, zda vás někdo pozval, mu prozradí, že jste tady, a proto to Amethyst nedělá na pozadí.</string> - <string name="cordn_invitations_load_failed">Pozvánky se nepodařilo načíst.</string> - <plurals name="cordn_invitations_members"> - <item quantity="one">%1$d osoba už je v této skupině</item> - <item quantity="few">%1$d osoby už jsou v této skupině</item> - <item quantity="many">%1$d osoby už je v této skupině</item> - <item quantity="other">%1$d osob už je v této skupině</item> - </plurals> - <string name="cordn_invitations_members_more">+%1$d další</string> - <string name="cordn_invitations_no_coordinators">Nemáte otevřeného žádného koordinátora. Pozvánky cordn přicházejí přes koordinátora, takže zatím není kde hledat.</string> - <string name="cordn_invitations_none">Žádné čekající pozvánky.</string> - <string name="cordn_invitations_refresh">Zkontrolovat znovu</string> - <string name="cordn_invitations_skipped">Pozvánka byla ponechána pro jiné zařízení</string> - <string name="cordn_invitations_title">Pozvánky Cordn</string> - <string name="cordn_invitations_unreachable">%1$s neodpověděl</string> - <string name="cordn_invitations_via">Přes %1$s</string> - <string name="cordn_keypackages_disclosure">Zveřejněním podepíšete záznam vlastním klíčem svého účtu. Daný koordinátor se pak dozví, že tento účet existuje a lze ho pozvat — natrvalo, a bez ohledu na to, zda vás někdo pozve.</string> - <string name="cordn_keypackages_explainer">Ostatní vás mohou přidat do skupiny cordn jen pomocí balíčku klíčů, který jste zveřejnili na daném koordinátoru. Cordn ho nemá kde jinde uchovat, takže bez zveřejněného balíčku vás nikdo nemůže pozvat — a nic mu neřekne proč.</string> - <string name="cordn_keypackages_failed">Balíčky klíčů se nepodařilo načíst.</string> - <string name="cordn_keypackages_last_resort_no">Žádný záložní balíček. Jakmile dojdou jednorázové, pozvánky budou selhávat.</string> - <string name="cordn_keypackages_last_resort_yes">Záložní balíček je zveřejněn, takže pozvánku lze vytvořit i poté, co dojdou jednorázové.</string> - <plurals name="cordn_keypackages_orphans"> - <item quantity="one">%1$d balíček, který toto zařízení nedokáže otevřít</item> - <item quantity="few">%1$d balíčky, které toto zařízení nedokáže otevřít</item> - <item quantity="many">%1$d balíčku, které toto zařízení nedokáže otevřít</item> - <item quantity="other">%1$d balíčků, které toto zařízení nedokáže otevřít</item> - </plurals> - <string name="cordn_keypackages_orphans_body">Koordinátor je předá každému, kdo vás zve, ale soukromá část není na tomto zařízení — patří jiné instalaci. Pokud tato instalace už neexistuje, pozvánka s jedním z nich vytvoří zprávu Welcome, kterou nikdo nikdy neotevře.</string> - <string name="cordn_keypackages_publish">Zveřejnit jeden</string> - <string name="cordn_keypackages_publish_last_resort">Zveřejnit záložní</string> - <plurals name="cordn_keypackages_summary"> - <item quantity="one">K dispozici je %1$d jednorázový balíček.</item> - <item quantity="few">K dispozici jsou %1$d jednorázové balíčky.</item> - <item quantity="many">K dispozici je %1$d jednorázového balíčku.</item> - <item quantity="other">K dispozici je %1$d jednorázových balíčků.</item> - </plurals> - <string name="cordn_keypackages_topup_note">Jakmile zde poprvé něco zveřejníte, Amethyst bude spotřebované balíčky potichu nahrazovat. Ten první ale nikdy nezveřejní za vás.</string> - <string name="cordn_keypackages_withdraw_all">Stáhnout vše</string> - <string name="cordn_keypackages_withdraw_orphans">Stáhnout tyto</string> - <string name="cordn_link_request">Požádat o připojení k této skupině</string> - <string name="cordn_link_request_disclosure">Tímto zveřejníte balíček klíčů pod vlastním klíčem svého účtu a sdělíte koordinátorovi, že se chcete připojit k této skupině — bez ohledu na to, zda někdo odpoví.</string> - <string name="cordn_link_request_failed">O připojení se nepodařilo požádat.</string> - <string name="cordn_link_request_sent">Požádáno. Některý člen skupiny vás musí přidat; pozvánka se pak objeví mezi pozvánkami cordn.</string> - <string name="cordn_link_scan">Skenovat</string> - <string name="cordn_media_download_failed">Tento soubor se nepodařilo otevřít.</string> - <string name="cordn_media_no_server">Pro tento účet není nastaven žádný mediální server, takže soubor nemá kam uložit. Vyberte ho v Nastavení.</string> - <string name="cordn_media_unreadable">Tento soubor nelze přečíst.</string> - <string name="cordn_media_upload_failed">Tento soubor se nepodařilo odeslat.</string> - <plurals name="cordn_member_count"> - <item quantity="one">%1$d člen</item> - <item quantity="few">%1$d členové</item> - <item quantity="many">%1$d člena</item> - <item quantity="other">%1$d členů</item> - </plurals> - <string name="cordn_message_deleted">Zpráva smazána</string> - <string name="cordn_message_details_coordinator">Koordinátor</string> - <string name="cordn_message_details_cursor">Kurzor</string> - <string name="cordn_message_details_sent_at">Odesláno</string> - <string name="cordn_message_details_title">Podrobnosti zprávy</string> - <string name="cordn_message_edited">upraveno</string> - <string name="cordn_pinned_by">Připnul %1$s</string> - <plurals name="cordn_pinned_count"> - <item quantity="one">%1$d připnutá zpráva v této skupině</item> - <item quantity="few">%1$d připnuté zprávy v této skupině</item> - <item quantity="many">%1$d připnuté zprávy v této skupině</item> - <item quantity="other">%1$d připnutých zpráv v této skupině</item> - </plurals> - <string name="cordn_pinned_next">Další připnutá zpráva</string> - <string name="cordn_pinned_previous">Předchozí připnutá zpráva</string> - <string name="cordn_pinned_show_all">Zobrazit všechny připnuté zprávy</string> - <string name="cordn_pinned_title">Připnuté zprávy</string> - <string name="cordn_reactions_title">Reakce</string> - <string name="cordn_requests_accept">Přidat</string> - <string name="cordn_requests_admin_only">Přijmout může jen administrátor. Ve skupině bez administrátorů to je kdokoli.</string> - <string name="cordn_requests_check">Zkontrolovat žádosti</string> - <string name="cordn_requests_decline">Zamítnout</string> - <string name="cordn_requests_failed">Žádosti se nepodařilo načíst.</string> - <string name="cordn_requests_none">Nikdo nečeká na připojení.</string> - <string name="cordn_requests_title">Žádosti o připojení</string> - <string name="cordn_send">Odeslat</string> - <string name="cordn_send_failed">Odeslání se nezdařilo.</string> - <string name="cordn_send_no_session">Koordinátor této skupiny není otevřen, takže zatím nelze nic odeslat.</string> - <string name="cordn_share_copied">Zkopírováno</string> - <string name="cordn_share_copy">Kopírovat odkaz</string> - <string name="cordn_share_explainer">Kdokoli s tímto odkazem může požádat o připojení. Pustit ho dovnitř ale musí administrátor.</string> - <string name="cordn_share_title">Sdílet tuto skupinu</string> - <string name="cordn_voice_play">Přehrát hlasovou zprávu</string> - <string name="cordn_voice_record">Nahrát hlasovou zprávu</string> - <string name="cordn_voice_stop">Zastavit a odeslat</string> - <string name="qr_scanner_camera_rationale">Amethyst potřebuje fotoaparát ke skenování QR kódů. Nic se nenahrává ani neodesílá — snímky se dekódují v zařízení a poté se zahodí.</string> - <string name="qr_scanner_grant_camera">Povolit fotoaparát</string> - <string name="qr_scanner_camera_blocked">Přístup k fotoaparátu je pro Amethyst vypnutý. Znovu ho můžete zapnout v nastavení Androidu.</string> - <string name="qr_scanner_open_settings">Otevřít nastavení</string> - <string name="qr_scanner_unavailable">Dekodér QR kódů se v tomto zařízení nepodařilo spustit. Kód můžete stále vložit jako text.</string> - <string name="qr_scanner_torch_on">Zapnout světlo</string> - <string name="qr_scanner_torch_off">Vypnout světlo</string> - <string name="qr_scanner_dark_hint">Je tma — zkuste světlo</string> - <string name="qr_scanner_scan_image">Naskenovat obrázek</string> - <string name="qr_scanner_paste">Vložit</string> - <string name="qr_scanner_no_code_in_image">V obrázku nebyl nalezen žádný QR kód</string> - <string name="qr_scanner_clipboard_empty">Ve schránce není nic ke skenování</string> - <string name="qr_scanner_pick_one">Klepněte na požadovaný kód</string> - <plurals name="qr_scanner_sequence_progress"> - <item quantity="one">Načteno %1$s z %2$s části — pokračujte</item> - <item quantity="few">Načteno %1$s z %2$s částí — pokračujte</item> - <item quantity="many">Načteno %1$s z %2$s části — pokračujte</item> - <item quantity="other">Načteno %1$s z %2$s částí — pokračujte</item> - </plurals> - <string name="qr_scanner_zoom_reset">Obnovit přiblížení</string> - <string name="qr_scanner_unsupported_title">Amethyst tento kód nedokáže otevřít</string> - <string name="qr_scanner_unsupported_secret">Tento kód obsahuje soukromý klíč nebo tajemství pro spárování, proto se jeho obsah zde nezobrazuje. Použijte obrazovku, která ho očekává.</string> - <string name="qr_scanner_kind_nsec">Toto je soukromý klíč. Vložte ho raději na přihlašovací obrazovce.</string> - <string name="qr_scanner_kind_wallet">Toto je připojení peněženky. Přidejte ho v nastavení peněženky.</string> - <string name="qr_scanner_kind_bunker">Toto je adresa vzdáleného podepisovatele. Amethyst je rozdává, aby ho ostatní aplikace mohly žádat o podpis — přihlásit se s ní nedokáže.</string> - <string name="qr_scanner_kind_signer">Toto je aplikace, která se chce připojit k vašemu podepisovateli. Otevřete ji z obrazovky podepisovatele NIP-46.</string> - <string name="qr_scanner_kind_lightning">Toto je Lightning faktura.</string> - <string name="qr_scanner_kind_cashu">Toto je Cashu token.</string> - <string name="qr_scanner_kind_web">Toto je webový odkaz.</string> - <string name="qr_scanner_kind_text">Toto je prostý text, ne kód Nostr.</string> - <string name="qr_scanner_kind_nostr_unsupported">Toto je kód Nostr, ale tato obrazovka ho nedokáže otevřít.</string> - <string name="qr_scanner_label_profile">Profil</string> - <string name="qr_scanner_label_note">Poznámka</string> - <string name="qr_scanner_label_nostr">Kód Nostr</string> - <string name="qr_scanner_label_relay">Relé</string> - <string name="qr_scanner_label_nsec">Soukromý klíč</string> - <string name="qr_scanner_label_wallet">Připojení peněženky</string> - <string name="qr_scanner_label_bunker">Adresa vzdáleného podepisovatele</string> - <string name="qr_scanner_label_signer">Žádost o připojení k podepisovateli</string> - <string name="qr_scanner_label_lightning">Lightning faktura</string> - <string name="qr_scanner_label_cashu">Cashu token</string> - <string name="qr_scanner_label_web">Webový odkaz</string> - <string name="qr_scanner_label_text">Prostý text</string> - <string name="qr_scanner_open_link">Otevřít odkaz</string> - <string name="qr_scanner_copy">Kopírovat</string> - <string name="qr_scanner_copied">Zkopírováno</string> - <string name="qr_scanner_try_again">Skenovat znovu</string> - <string name="workout_suggestion_connect_title">Využijte tréninky, které už máte zaznamenané</string> - <string name="workout_suggestion_connect_message">Připojte Health Connect (Samsung Health, Google Fit, Fitbit, Garmin…) — získáte Moji kondici, vlastní přehled tréninků, a tento trénink vyplníte podle záznamu z hodinek místo ručního psaní každého čísla.</string> - <string name="health_connect_rationale_headline">Amethyst používá Health Connect pro funkci Moje kondice — váš vlastní přehled tréninků přímo v telefonu.</string> - <string name="health_connect_rationale_intro">Moje kondice (menu → Vy → Moje kondice) ukazuje, jak trénujete: kolik jste toho udělali tento týden oproti minulému, jak se váš čas dělí mezi běh, cyklistiku, chůzi, plavání a posilovnu, vaše nejlepší výkony, kolik dní jste trénovali a vaši aktuální sérii. Amethyst tento obraz sestaví z tréninků, které vaše hodinky nebo fitness aplikace už uložily do Health Connect, spolu s tréninky, které jste zaznamenali přímo v Amethystu. Vše se vypočítá ve vašem telefonu a zobrazí jen vám.</string> - <string name="health_connect_rationale_exercise">Cvičení · jakou aktivitu jste dělali, kdy začala a jak dlouho trvala. To je samotný trénink: určuje počet tréninků, dobu tréninku, rozpis podle aktivit, aktivní dny a vaši sérii.</string> - <string name="health_connect_rationale_distance">Vzdálenost · jak daleko jste se dostali; zobrazuje se jako vzdálenost tento týden oproti minulému, týdenní průměr, vzdálenost podle aktivity a nejdelší vzdálenost.</string> - <string name="health_connect_rationale_calories">Aktivní a celkové kalorie · energie spálená při tréninku; zobrazuje se jako energie tento týden a její srovnání s minulým týdnem. Aktivní kalorie se použijí, pokud je váš zdroj zaznamenává; celkové kalorie slouží jako náhrada u zdrojů, které zaznamenávají jen celkovou energii.</string> - <string name="health_connect_rationale_heart_rate">Tepová frekvence · průměr a maximum během každého tréninku, sloučené do průměrné a nejvyšší tepové frekvence za období. Podle ní poznáte lehký týden od náročného při stejné vzdálenosti.</string> - <string name="health_connect_rationale_steps">Kroky · počet kroků při běhu, chůzi nebo túře; zobrazuje se jako týdenní průměr kroků a nejvyšší počet kroků.</string> - <string name="health_connect_rationale_elevation">Nastoupané převýšení · kolik jste nastoupali; zobrazuje se jako týdenní průměr stoupání a největší stoupání. Právě to odlišuje rovinatý týden od kopcovitého.</string> - <string name="health_connect_rationale_limit_window">Čte jen tréninky z posledních 4 týdnů, a to jen když je otevřená Moje kondice nebo editor tréninku. Nikdy nečte na pozadí.</string> - <string name="health_connect_rationale_limit_publish">Vaše zdravotní údaje nikdy nikam neposílá. Moje kondice se počítá ve vašem telefonu a zůstává v něm. Pokud později budete chtít zveřejnit jeden trénink, klepnete u něj na Sdílet, přečtete si příspěvek, který Amethyst vyplnil, a zveřejníte ho sami — vždy jen jeden trénink, nikdy automaticky.</string> - <string name="health_connect_rationale_limit_optional">Celá funkce je volitelná. Oprávnění můžete kdykoli odvolat v Health Connect — Moje kondice se pak vrátí k úvodní výzvě — nebo vypnout návrhy v editoru v Nastavení → Nastavení editoru. Zbytek Amethystu funguje dál.</string> - <string name="my_fitness_title">Moje kondice</string> - <string name="my_fitness_window">Poslední 4 týdny</string> - <string name="my_fitness_loading_metrics">Načítám vzdálenost, kalorie a tepovou frekvenci…</string> - <string name="my_fitness_window_note">Poslední čtyři celé týdny vašeho tréninku: tréninky, které jste zveřejnili, a vše, co Health Connect zaznamenává v tomto telefonu. Amethyst přehled počítá v zařízení a nic z něj nezveřejňuje.</string> - <string name="my_fitness_this_week">Tento týden</string> - <string name="my_fitness_vs_last_week">oproti minulému týdnu</string> - <string name="my_fitness_weekly_average">Týdenní průměr</string> - <string name="my_fitness_workouts">Tréninky</string> - <string name="my_fitness_active_days">Aktivní dny</string> - <string name="my_fitness_streak">Série dní</string> - <string name="my_fitness_time">Čas</string> - <string name="my_fitness_duration_hours_minutes">%1$d h %2$d min</string> - <string name="my_fitness_duration_minutes">%1$d min</string> - <string name="my_fitness_duration_seconds">%1$d s</string> - <string name="my_fitness_distance">Vzdálenost</string> - <string name="my_fitness_calories">Kalorie</string> - <string name="my_fitness_steps">Kroky</string> - <string name="my_fitness_elevation">Stoupání</string> - <string name="my_fitness_avg_heart_rate">Prům. TF</string> - <string name="my_fitness_max_heart_rate">Max. TF</string> - <string name="my_fitness_by_activity">Podle aktivity</string> - <string name="my_fitness_bests">Nejlepší výkony</string> - <string name="my_fitness_best_longest_distance">Nejdelší vzdálenost</string> - <string name="my_fitness_best_longest_duration">Nejdelší trénink</string> - <string name="my_fitness_best_biggest_climb">Největší stoupání</string> - <string name="my_fitness_best_most_steps">Nejvíce kroků</string> - <string name="my_fitness_best_highest_heart_rate">Nejvyšší tepová frekvence</string> - <string name="my_fitness_recent">Nedávné tréninky</string> - <string name="my_fitness_empty">Za poslední 4 týdny nic zaznamenáno. Zobrazí se zde tréninky, které zveřejníte, i ty, které zaznamenají vaše hodinky.</string> - <string name="my_fitness_connect_title">Sledujte svůj trénink</string> - <string name="my_fitness_connect_message">Připojte Health Connect a uvidíte týdenní součty, vývoj oproti minulému týdnu, nejlepší výkony a tréninkovou sérii. Vše zůstává ve vašem telefonu.</string> - <string name="my_fitness_connect_banner">Připojte Health Connect a přidejte tréninky zaznamenané hodinkami, včetně tepové frekvence, kroků a stoupání.</string> - <string name="my_fitness_connect_button">Připojit</string> - <string name="my_fitness_share">Sdílet tento trénink</string> - <string name="my_fitness_unit_bpm">tep/min</string> - <string name="my_fitness_unit_kcal">kcal</string> - <string name="my_fitness_unit_km">km</string> - <string name="my_fitness_unit_mi">mi</string> - <string name="my_fitness_unit_m">m</string> - <string name="my_fitness_unit_ft">ft</string> - <string name="looking_for_event_title">Načítání</string> - <string name="looking_for_private_event">Čekám na načtení vašich soukromých zpráv…</string> - <string name="call_duration_minutes_seconds">%1$02d:%2$02d</string> - <string name="call_duration_hours_minutes_seconds">%1$d:%2$02d:%3$02d</string> - <string name="marmot_preview_group_updated">Skupina aktualizována</string> - <string name="marmot_preview_media">Příloha</string> - <string name="marmot_preview_no_text">Zpráva</string> - <string name="marmot_preview_with_sender">%1$s: %2$s</string> - <string name="marmot_empty_create_action">Vytvořit skupinu</string> - <string name="marmot_invite_device_banner">Pozvánky Marmot chodí na jiné zařízení a zde je nelze otevřít.</string> - <string name="marmot_invite_device_banner_dismiss">Zavřít</string> - <string name="marmot_invite_device_publish">Zveřejnit z tohoto zařízení</string> - <string name="wallet_open_zapped_note">Otevřít poznámku se zapem</string> - <string name="video_player_settings_action_captions">Titulky</string> - <string name="video_player_settings_action_captions_description">Zapnout nebo vypnout titulky (skryto, pokud je video nemá)</string> - <string name="app_notification_reply_failed">Neodesláno</string> - <string name="app_notification_reply_retry">Zkusit znovu</string> - <string name="app_notification_reply_sending">Odesílání…</string> - <string name="app_notification_reply_unconfirmed">Stále se odesílá…</string> - <string name="app_notification_shortcut_account_removed">Tento účet byl z Amethystu odebrán.</string> - <string name="captions_off">Vypnuto</string> - <string name="captions_turn_off">Vypnout titulky</string> - <string name="captions_turn_on">Zapnout titulky</string> - <string name="captions_unnamed_track">Titulky</string> - <string name="kind_app_curation_set">Seznam aplikací</string> - <string name="kind_article_curation_set">Seznam článků</string> - <string name="kind_bookmark_set">Sada záložek</string> - <string name="kind_favorite_algo_feeds">Oblíbené kanály</string> - <string name="kind_git_authors">Git autoři</string> - <string name="kind_git_repositories">Git repozitáře</string> - <string name="kind_good_wiki_authors">Důvěryhodní autoři wiki</string> - <string name="kind_good_wiki_relays">Důvěryhodná wiki relé</string> - <string name="kind_interest_set">Zájmy</string> - <string name="kind_media_follows">Sledování médií</string> - <string name="kind_media_starter_pack">Startovní balíček médií</string> - <string name="kind_mute_set_for_kind">Ztlumeno pro druh %1$s</string> - <string name="kind_picture_curation_set">Seznam obrázků</string> - <string name="kind_simple_groups">Skupiny</string> - <string name="kind_video_collaboration">Spolupráce na videu</string> - <string name="kind_video_list">Seznam videí</string> - <string name="kind_video_subtitles">Titulky videa</string> - <string name="login_bunker_not_supported">Amethyst pro Android se nemůže přihlásit pomocí adresy bunker://. Tyto adresy rozdává, aby ho ostatní aplikace mohly žádat o podpis, ale přihlásit se s nimi nedokáže. Použijte externí podepisovací aplikaci nebo Amethyst Desktop.</string> - <string name="login_nostrconnect_not_supported">Toto je aplikace, která se chce připojit k vašemu podepisovateli, ne způsob přihlášení. Nejprve se přihlaste a pak ji otevřete z obrazovky podepisovatele NIP-46.</string> - <string name="my_fitness">Moje kondice</string> - <string name="nip51_list_has_private_members">Tento seznam má soukromé členy, které vidí jen jeho autor</string> - <string name="route_edit_geocache">Upravit keš</string> - <string name="route_edit_geocache_hunt">Upravit výpravu</string> - <string name="route_geocache_detail">Keš</string> - <string name="route_geocache_hunt_detail">Výprava</string> - <string name="route_geocache_hunts">Výpravy</string> - <string name="route_geocaches">Keše</string> - <string name="route_log_geocache_find">Zapsat nález</string> - <string name="route_new_geocache">Ukrýt keš</string> - <string name="route_new_geocache_hunt">Nová výprava</string> - <string name="video_collaboration_accepted_on">Přijal uvedení jako spolupracovník</string> - <string name="video_collaboration_declined_on">Odmítl uvedení jako spolupracovník</string> - <string name="video_credit_accepted">Uvedení přijato</string> - <string name="video_credit_audio_from">Zvuk od</string> - <string name="video_credit_featuring">Účinkuje</string> - <string name="video_credit_inspired_by">Inspirováno</string> - <string name="video_credit_references">Odkazuje na</string> - <string name="video_curation_set_private_items">Soukromý seznam</string> - <plurals name="video_curation_set_video_count"> - <item quantity="one">%1$d video</item> - <item quantity="few">%1$d videa</item> - <item quantity="many">%1$d videa</item> - <item quantity="other">%1$d videí</item> - </plurals> - <string name="marmot_invite_device_success">Toto zařízení nyní přijímá vaše pozvánky Marmot.</string> - <string name="marmot_invite_device_failure">Zveřejnění KeyPackage selhalo: %1$s</string> - <string name="marmot_invite_device_rejected">Žádné relé KeyPackage nepřijalo. Zkontrolujte nastavení relé a zkuste to znovu.</string> - <string name="chat_delivery_sending">Odesílání…</string> - <string name="chat_delivery_failed">Nepodařilo se odeslat. Klepnutím to zkusíte znovu.</string> - <plurals name="marmot_unread_messages"> - <item quantity="one">%1$d nová zpráva</item> - <item quantity="few">%1$d nové zprávy</item> - <item quantity="many">%1$d nové zprávy</item> - <item quantity="other">%1$d nových zpráv</item> - </plurals> - <string name="chat_type_cordn_title">Skupiny Cordn</string> - <string name="chat_type_cordn_desc">End-to-end šifrované skupiny, jejichž pořadí zpráv určuje vámi zvolený koordinátor, nikoli relé.</string> - <string name="new_conversation_cordn_title">Skupina Cordn</string> - <string name="new_conversation_cordn_tagline">Šifrovaný skupinový chat řazený koordinátorem, kterého si vyberete.</string> - <string name="new_conversation_cordn_chip">Koordinovaná</string> - <string name="new_conversation_cordn_best">Nejlepší pro týmy, které chtějí jedno spolehlivé pořadí konverzace.</string> - <string name="new_conversation_cordn_cta">Vytvořit skupinu Cordn</string> - <string name="new_conversation_cordn_pro_1">End-to-end šifrování pomocí MLS — koordinátor nikdy nepřečte žádnou zprávu.</string> - <string name="new_conversation_cordn_pro_2">Jedno dohodnuté pořadí pro všechny, takže historii nemohou přeházet špatně nastavené hodiny.</string> - <string name="new_conversation_cordn_con_1">Koordinátor zjistí, kdo je ve skupině a kdy spolu mluvíte, i když nemůže číst, co píšete.</string> <string name="sno_object_title">3D objekt</string> <string name="sno_object_details">%1$d vrcholů · %2$d stěn</string> <string name="sno_object_unreadable">Tento 3D objekt nelze přečíst (pravidlo %1$s)</string> @@ -5932,6 +5637,7 @@ <item quantity="many">%1$d bajtu něčeho, co tento klient neumí přečíst</item> <item quantity="other">%1$d bajtů něčeho, co tento klient neumí přečíst</item> </plurals> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> <string name="browser_pill_close">Zavřít</string> <string name="browser_pill_tile_copy">Kopírovat odkaz</string> <string name="browser_pill_tile_find">Najít</string> @@ -6426,4 +6132,298 @@ <string name="backup_type_trust_providers_explainer">Služby, kterým důvěřujete, že pro vás hodnotí a ověřují lidi.</string> <string name="backup_type_trusted_relays">seznam důvěryhodných relé</string> <string name="backup_type_trusted_relays_explainer">Relé, ke kterým se připojujete bez dotazu.</string> + <string name="cordn_action_delete">Smazat</string> + <string name="cordn_action_edit">Upravit</string> + <string name="cordn_action_editing">Úprava vaší zprávy</string> + <string name="cordn_action_go_to_message">Přejít na zprávu</string> + <string name="cordn_action_pin">Připnout</string> + <string name="cordn_action_reply">Odpovědět</string> + <string name="cordn_action_unpin">Odepnout</string> + <string name="cordn_admin_action_failed">Tuto změnu nebylo možné provést.</string> + <string name="cordn_admin_coordinator_silent">Koordinátor neodpověděl, takže se nic nezměnilo. Zkuste to za chvíli znovu.</string> + <string name="cordn_admin_no_key_package">%1$s na tomto koordinátoru nezveřejnil balíček klíčů, takže ho zatím nelze přidat. Požádejte ho, aby na tomto koordinátoru nejprve otevřel cordn.</string> + <string name="cordn_admin_no_self_remove">Ze skupiny cordn nemůžete odebrat sami sebe.</string> + <string name="cordn_admin_no_welcome">Pro %1$s se nepodařilo zajistit přístup do skupiny, takže přidání bylo zrušeno.</string> + <string name="cordn_admin_not_a_member">%1$s není v této skupině.</string> + <string name="cordn_admin_someone">Tato osoba</string> + <string name="cordn_admin_wrong_key_package">Tento koordinátor poskytl balíček klíčů, který patří někomu jinému, takže %1$s nebyl přidán. Nic se nezměnilo.</string> + <string name="cordn_backup_contents_body">Šifrovací klíče vašich skupin, vaše balíčky klíčů a všechny zprávy v těchto skupinách. Kdokoli soubor otevře, může konverzace číst. Jakmile soubor opustí Amethyst, chrání ho jen heslo.</string> + <string name="cordn_backup_contents_title">Co soubor obsahuje</string> + <string name="cordn_backup_explainer">Váš nsec skupiny cordn neobnoví. Skupina cordn existuje jako šifrovací stav na tomto zařízení a jako seřazený proud zpráv na koordinátoru, který ho nedokáže přečíst — přijdete-li o zařízení bez zálohy, skupina je pryč, včetně všeho, co v ní už zaznělo.</string> + <string name="cordn_backup_export">Exportovat</string> + <string name="cordn_backup_exported">Záloha uložena.</string> + <string name="cordn_backup_failed">Nepovedlo se. Zkontrolujte heslo a soubor.</string> + <string name="cordn_backup_passphrase">Heslo</string> + <string name="cordn_backup_restore">Obnovit</string> + <string name="cordn_backup_restore_body">Obnovení nahradí skupiny cordn na tomto zařízení těmi ze souboru. Je určeno pro telefon, který převzal roli jiného — ne k používání stejných skupin na dvou zařízeních zároveň.</string> + <string name="cordn_backup_restore_confirm_body">Všechny skupiny cordn, které jsou nyní na tomto zařízení, budou odebrány a nahrazeny těmi ze souboru.\n\nNeobnovujte zálohu na druhý telefon, dokud se ten první stále používá. Oba by měly stejné klíče skupin, a jakmile oba odešlou zprávu, skupina se rozbije pro všechny — potichu a nevratně.</string> + <string name="cordn_backup_restore_confirm_title">Nahradit skupiny cordn na tomto zařízení?</string> + <string name="cordn_backup_restore_title">Obnovit</string> + <string name="cordn_backup_restored">Obnoveno.</string> + <string name="cordn_chat_empty_description">Vše, co sem pošlete, je zašifrováno pro tuto skupinu. Koordinátor to předává dál, aniž by to mohl přečíst.</string> + <string name="cordn_chat_empty_title">Zatím žádné zprávy</string> + <string name="cordn_chat_unread_divider">Nové zprávy</string> + <string name="cordn_chat_yesterday">Včera</string> + <string name="cordn_composer_hint">Zpráva</string> + <string name="cordn_coordinators_add_action">Přidat</string> + <string name="cordn_coordinators_add_failed">Tohoto koordinátora se nepodařilo otevřít.</string> + <string name="cordn_coordinators_copy_key">Kopírovat npub koordinátora</string> + <string name="cordn_coordinators_discover_action">Hledat koordinátory</string> + <string name="cordn_coordinators_discover_add">Přidat</string> + <string name="cordn_coordinators_discover_explainer">Čte oznámení, která už vaše relaye přenášejí. Žádný koordinátor není kontaktován, takže se žádný z nich nedozví, že jste hledali.</string> + <string name="cordn_coordinators_discover_failed">Oznámení se nepodařilo načíst</string> + <string name="cordn_coordinators_discover_none">Na vašich relayích se nikdo neohlašuje.</string> + <string name="cordn_coordinators_discover_seen">Ohlášeno před %1$s</string> + <string name="cordn_coordinators_discover_seen_on">Naposledy ohlášeno %1$s</string> + <plurals name="cordn_coordinators_discover_unheard"> + <item quantity="one">Nic nenalezeno a %1$d z vašich relayů neodpověděl.</item> + <item quantity="few">Nic nenalezeno a %1$d z vašich relayů neodpověděly.</item> + <item quantity="many">Nic nenalezeno a %1$d z vašich relayů neodpovědělo.</item> + <item quantity="other">Nic nenalezeno a %1$d z vašich relayů neodpovědělo.</item> + </plurals> + <string name="cordn_coordinators_explainer">Koordinátor určuje pořadí zpráv jedné skupiny a nic jiného to dělat nemůže. Jeho ztrátou se ztratí i pořadí a druhý koordinátor ten první nezrcadlí — proto jde o konkrétní servery, které si zvolíte, ne o zaměnitelné relaye.</string> + <plurals name="cordn_coordinators_health_down"> + <item quantity="one">Neodpovídá — %1$d požadavek za sebou selhal. Skupiny na něm se nesynchronizují.</item> + <item quantity="few">Neodpovídá — %1$d požadavky za sebou selhaly. Skupiny na něm se nesynchronizují.</item> + <item quantity="many">Neodpovídá — %1$d požadavku za sebou selhalo. Skupiny na něm se nesynchronizují.</item> + <item quantity="other">Neodpovídá — %1$d požadavků za sebou selhalo. Skupiny na něm se nesynchronizují.</item> + </plurals> + <string name="cordn_coordinators_health_ok">Odpovídá.</string> + <string name="cordn_coordinators_health_retrying">Požadavek selhal, zkouší se znovu.</string> + <string name="cordn_coordinators_health_unknown">Zatím nebyl o nic požádán.</string> + <string name="cordn_coordinators_hide_older">Skrýt ty, které se přestaly ohlašovat</string> + <string name="cordn_coordinators_identify">Zjistit, kdo to je</string> + <string name="cordn_coordinators_key">Klíč koordinátora</string> + <string name="cordn_coordinators_label">Název (váš vlastní, volitelný)</string> + <string name="cordn_coordinators_label_note">Tento název je váš a zůstává na tomto zařízení. Koordinátor nemůže název prokázat, proto se ho na něj nikdy neptáme.</string> + <string name="cordn_coordinators_more">Další akce</string> + <string name="cordn_coordinators_none">Zatím žádní koordinátoři.</string> + <string name="cordn_coordinators_purge">Vymazat</string> + <string name="cordn_coordinators_purge_body">Odebráním zůstanou skupiny na tomto zařízení, takže po opětovném přidání koordinátora se vrátí s ním. Vymazáním se smažou: klíče skupin, pozice čtení i vaše balíčky klíčů zmizí a do těchto skupin se lze vrátit jen s novou pozvánkou.\n\nKoordinátor se o tom nedozví a ponechá si vše, co už měl.</string> + <string name="cordn_coordinators_purge_title">Vymazat tohoto koordinátora?</string> + <string name="cordn_coordinators_relays">%1$s</string> + <string name="cordn_coordinators_relays_label">Odpovídá na</string> + <string name="cordn_coordinators_relays_more">%1$s +%2$d další</string> + <string name="cordn_coordinators_remove">Odebrat</string> + <string name="cordn_coordinators_rename">Přejmenovat</string> + <string name="cordn_coordinators_server">Tvrdí, že je: %1$s</string> + <string name="cordn_coordinators_server_claim">Název a verzi si koordinátor zvolil sám. Identifikuje ho jen jeho veřejný klíč.</string> + <string name="cordn_coordinators_server_silent">Odpověděl, ale nic neuvedl.</string> + <string name="cordn_coordinators_show_all">Zobrazit vše (%1$d)</string> + <string name="cordn_coordinators_show_fewer">Zobrazit méně</string> + <plurals name="cordn_coordinators_show_older"> + <item quantity="one">Zobrazit %1$d, který se přestal ohlašovat</item> + <item quantity="few">Zobrazit %1$d, které se přestaly ohlašovat</item> + <item quantity="many">Zobrazit %1$d, které se přestaly ohlašovat</item> + <item quantity="other">Zobrazit %1$d, které se přestaly ohlašovat</item> + </plurals> + <string name="cordn_coordinators_stale_note">Tito koordinátoři se neohlásili déle než měsíc. Vytvoření skupiny na takovém, který už zanikl, selže.</string> + <string name="cordn_create_action">Vytvořit skupinu</string> + <plurals name="cordn_create_action_invite"> + <item quantity="one">Vytvořit a pozvat %1$d osobu</item> + <item quantity="few">Vytvořit a pozvat %1$d osoby</item> + <item quantity="many">Vytvořit a pozvat %1$d osoby</item> + <item quantity="other">Vytvořit a pozvat %1$d osob</item> + </plurals> + <string name="cordn_create_add_member">Přidat někoho</string> + <string name="cordn_create_admin">Administrátor</string> + <plurals name="cordn_create_admin_count"> + <item quantity="one">%1$d osoba může přidávat a odebírat</item> + <item quantity="few">%1$d osoby mohou přidávat a odebírat</item> + <item quantity="many">%1$d osoby může přidávat a odebírat</item> + <item quantity="other">%1$d osob může přidávat a odebírat</item> + </plurals> + <string name="cordn_create_admin_only_me">Tuto skupinu spravuji jen já</string> + <string name="cordn_create_admin_only_me_on">Jen vy můžete zvát, odebírat členy nebo měnit údaje skupiny. Později to můžete předat někomu jinému.</string> + <string name="cordn_create_coordinator">Koordinátor</string> + <string name="cordn_create_coordinator_change">Změnit</string> + <string name="cordn_create_coordinator_discover">Najít koordinátory</string> + <string name="cordn_create_coordinator_hide">Hotovo</string> + <string name="cordn_create_coordinator_new">Jiný koordinátor</string> + <string name="cordn_create_coordinator_pubkey">Veřejný klíč koordinátora (hex nebo npub)</string> + <string name="cordn_create_coordinator_relays">Relaye, na kterých odpovídá, jeden na řádek</string> + <plurals name="cordn_create_coverage_all"> + <item quantity="one">Zastihne vaši %1$d osobu</item> + <item quantity="few">Zastihne všechny %1$d vaše osoby</item> + <item quantity="many">Zastihne %1$d vaší osoby</item> + <item quantity="other">Zastihne všech %1$d vašich osob</item> + </plurals> + <string name="cordn_create_coverage_partial">Zastihne %1$d z %2$d — ostatní by zůstali mimo</string> + <string name="cordn_create_coverage_unknown">Neověřeno — výběrem se to zjistí</string> + <string name="cordn_create_description">Popis (volitelné)</string> + <string name="cordn_create_egalitarian">Všichni mohou přidávat a odebírat</string> + <string name="cordn_create_egalitarian_note">Každý ve skupině může zvát, odebírat členy a měnit její údaje. Kterýkoli člen může později jmenovat administrátory, čímž tento režim skončí.</string> + <string name="cordn_create_explainer">Pořadí zpráv ve skupině cordn určuje jeden koordinátor, kterého si zvolíte. Skupina se vytváří na tomto zařízení — koordinátor se o její existenci dozví, až někoho pozvete nebo pošlete zprávu.</string> + <string name="cordn_create_failed">Skupinu se nepodařilo vytvořit.</string> + <string name="cordn_create_name">Název skupiny</string> + <string name="cordn_create_no_coordinator">Není zvolen žádný koordinátor</string> + <string name="cordn_create_outcome_explainer">Každá pozvánka je zpráva Welcome ponechaná na koordinátoru, takže mohou selhat i jednotlivě. Nikdo nedostal oznámení: pozvánku uvidí, až příště otevřou svou aplikaci.</string> + <string name="cordn_create_outcome_failed">Koordinátor pozvánku odmítl</string> + <string name="cordn_create_outcome_link">Poslat odkaz</string> + <string name="cordn_create_outcome_no_welcome">Přidán do skupiny, ale pozvánka ho nezastihla — zkuste to znovu z obrazovky skupiny</string> + <string name="cordn_create_outcome_open">Otevřít chat</string> + <string name="cordn_create_outcome_skipped">Zde není žádný klíč, takže nešlo ponechat zprávu Welcome</string> + <string name="cordn_create_outcome_title">Skupina existuje</string> + <string name="cordn_create_outcome_waiting">Čeká se na ně</string> + <string name="cordn_create_people_done">Hotovo</string> + <string name="cordn_create_people_empty">Zatím nikdo nepřidán. Skupinu lze vytvořit prázdnou a pozvánky poslat později, ale právě uvedením lidí teď zjistíte, který koordinátor je zastihne.</string> + <string name="cordn_create_people_none">Zatím nikdo — klepnutím přidáte lidi</string> + <string name="cordn_create_people_note">Uvedením lidí teď zjistíme, který koordinátor je zastihne. Každý z nich dostane zprávu Welcome ponechanou na tomto koordinátoru a vyzvedne si ji, až příště otevře svou aplikaci.</string> + <plurals name="cordn_create_reach_count"> + <item quantity="one">Dostupný na %1$d koordinátoru</item> + <item quantity="few">Dostupný na %1$d koordinátorech</item> + <item quantity="many">Dostupný na %1$d koordinátoru</item> + <item quantity="other">Dostupný na %1$d koordinátorech</item> + </plurals> + <string name="cordn_create_reach_none">Žádný klíč na žádném z vašich koordinátorů</string> + <string name="cordn_create_reach_unknown">Zatím neověřeno</string> + <string name="cordn_create_step_admins">Kdo může přidávat a odebírat</string> + <string name="cordn_create_step_name">Pojmenování</string> + <string name="cordn_create_step_people">Kdo v ní bude</string> + <string name="cordn_create_step_where">Kde poběží</string> + <string name="cordn_create_title">Nová skupina Cordn</string> + <plurals name="cordn_create_unreachable_count"> + <item quantity="one">%1$d osobu nezastihne žádný z vašich koordinátorů</item> + <item quantity="few">%1$d osoby nezastihne žádný z vašich koordinátorů</item> + <item quantity="many">%1$d osoby nezastihne žádný z vašich koordinátorů</item> + <item quantity="other">%1$d osob nezastihne žádný z vašich koordinátorů</item> + </plurals> + <string name="cordn_create_you">Vy, vždy administrátor</string> + <string name="cordn_delete_confirm_body">Všichni ve skupině uvidí tuto zprávu jako staženou a Amethyst přestane zobrazovat její text. Koordinátor si stále uchovává zašifrovanou kopii, v jaké byla odeslána, a kdo ji už četl, ten ji už četl.</string> + <string name="cordn_delete_confirm_title">Stáhnout zprávu</string> + <string name="cordn_delivery_accepted">Přijato koordinátorem</string> + <string name="cordn_exposure_close">Rozumím</string> + <string name="cordn_exposure_open">Co tento koordinátor vidí</string> + <string name="cordn_group_info">Informace o skupině</string> + <string name="cordn_group_unavailable">Tato skupina není dostupná. Její koordinátor mohl být odebrán.</string> + <string name="cordn_group_unavailable_detail">Přidejte koordinátora v Nastavení, abyste mohli začít používat skupiny cordn.</string> + <plurals name="cordn_groups_count"> + <item quantity="one">%1$d skupina</item> + <item quantity="few">%1$d skupiny</item> + <item quantity="many">%1$d skupiny</item> + <item quantity="other">%1$d skupin</item> + </plurals> + <string name="cordn_groups_down_note">Z těchto skupin nic nepřichází. Amethyst to stále zkouší znovu.</string> + <string name="cordn_groups_manage">Koordinátoři</string> + <string name="cordn_groups_none">Zatím žádné skupiny cordn</string> + <string name="cordn_groups_none_detail">Skupina cordn žije na jednom koordinátoru, který spravuje její členství a předává zprávy, aniž by kdy viděl, co kdo píše.</string> + <string name="cordn_groups_start">Založit skupinu</string> + <string name="cordn_info_add_member">Přidat někoho</string> + <string name="cordn_info_add_member_none">Pod tímto jménem nikdo nenalezen.</string> + <string name="cordn_info_add_member_note">Přidat lze jen ty, kdo na tomto koordinátoru zveřejnili balíček klíčů.</string> + <string name="cordn_info_add_member_placeholder">Jméno, npub nebo jmeno@domena</string> + <string name="cordn_info_admin_badge">Admin</string> + <plurals name="cordn_info_admins_kept"> + <item quantity="one">%1$d administrátor skupiny zůstává beze změny.</item> + <item quantity="few">%1$d administrátoři skupiny zůstávají beze změny.</item> + <item quantity="many">%1$d administrátora skupiny zůstává beze změny.</item> + <item quantity="other">%1$d administrátorů skupiny zůstává beze změny.</item> + </plurals> + <string name="cordn_info_coordinator">Koordinátor</string> + <string name="cordn_info_coordinator_key">Klíč koordinátora</string> + <string name="cordn_info_coordinator_nprofile">Odkaz na koordinátora</string> + <string name="cordn_info_copy_nprofile">Kopírovat nprofile koordinátora</string> + <string name="cordn_info_edit_details">Upravit údaje</string> + <string name="cordn_info_egalitarian">Tato skupina nemá administrátory: každý může přidávat, odebírat a přejmenovávat, a to natrvalo.</string> + <string name="cordn_info_epoch">Epocha</string> + <string name="cordn_info_gid">ID skupiny</string> + <string name="cordn_info_has_key_package">Lze přidat na tomto koordinátoru</string> + <string name="cordn_info_members">Členové</string> + <string name="cordn_info_remove_confirm_body">%1$s odsud přestane dostávat zprávy. Co už přečetl, mu zůstane, a nelze to vrátit zpět — musel by být pozván znovu.</string> + <string name="cordn_info_remove_confirm_title">Odebrat ze skupiny?</string> + <string name="cordn_info_remove_member">Odebrat</string> + <string name="cordn_info_save">Uložit</string> + <string name="cordn_info_technical">Technické podrobnosti</string> + <string name="cordn_invitations_accept">Připojit se</string> + <string name="cordn_invitations_decline">Odmítnout</string> + <string name="cordn_invitations_decline_warning">Odmítnutí je trvalé. Pro návrat budete potřebovat novou pozvánku.</string> + <string name="cordn_invitations_explainer">Kontroluje se jen při otevření této obrazovky a nikdy jindy — dotaz na koordinátora, zda vás někdo pozval, mu prozradí, že jste tady, a proto to Amethyst nedělá na pozadí.</string> + <string name="cordn_invitations_load_failed">Pozvánky se nepodařilo načíst.</string> + <plurals name="cordn_invitations_members"> + <item quantity="one">%1$d osoba už je v této skupině</item> + <item quantity="few">%1$d osoby už jsou v této skupině</item> + <item quantity="many">%1$d osoby už je v této skupině</item> + <item quantity="other">%1$d osob už je v této skupině</item> + </plurals> + <string name="cordn_invitations_members_more">+%1$d další</string> + <string name="cordn_invitations_no_coordinators">Nemáte otevřeného žádného koordinátora. Pozvánky cordn přicházejí přes koordinátora, takže zatím není kde hledat.</string> + <string name="cordn_invitations_none">Žádné čekající pozvánky.</string> + <string name="cordn_invitations_refresh">Zkontrolovat znovu</string> + <string name="cordn_invitations_skipped">Pozvánka byla ponechána pro jiné zařízení</string> + <string name="cordn_invitations_title">Pozvánky Cordn</string> + <string name="cordn_invitations_unreachable">%1$s neodpověděl</string> + <string name="cordn_invitations_via">Přes %1$s</string> + <string name="cordn_keypackages_disclosure">Zveřejněním podepíšete záznam vlastním klíčem svého účtu. Daný koordinátor se pak dozví, že tento účet existuje a lze ho pozvat — natrvalo, a bez ohledu na to, zda vás někdo pozve.</string> + <string name="cordn_keypackages_explainer">Ostatní vás mohou přidat do skupiny cordn jen pomocí balíčku klíčů, který jste zveřejnili na daném koordinátoru. Cordn ho nemá kde jinde uchovat, takže bez zveřejněného balíčku vás nikdo nemůže pozvat — a nic mu neřekne proč.</string> + <string name="cordn_keypackages_failed">Balíčky klíčů se nepodařilo načíst.</string> + <string name="cordn_keypackages_last_resort_no">Žádný záložní balíček. Jakmile dojdou jednorázové, pozvánky budou selhávat.</string> + <string name="cordn_keypackages_last_resort_yes">Záložní balíček je zveřejněn, takže pozvánku lze vytvořit i poté, co dojdou jednorázové.</string> + <plurals name="cordn_keypackages_orphans"> + <item quantity="one">%1$d balíček, který toto zařízení nedokáže otevřít</item> + <item quantity="few">%1$d balíčky, které toto zařízení nedokáže otevřít</item> + <item quantity="many">%1$d balíčku, které toto zařízení nedokáže otevřít</item> + <item quantity="other">%1$d balíčků, které toto zařízení nedokáže otevřít</item> + </plurals> + <string name="cordn_keypackages_orphans_body">Koordinátor je předá každému, kdo vás zve, ale soukromá část není na tomto zařízení — patří jiné instalaci. Pokud tato instalace už neexistuje, pozvánka s jedním z nich vytvoří zprávu Welcome, kterou nikdo nikdy neotevře.</string> + <string name="cordn_keypackages_publish">Zveřejnit jeden</string> + <string name="cordn_keypackages_publish_last_resort">Zveřejnit záložní</string> + <plurals name="cordn_keypackages_summary"> + <item quantity="one">K dispozici je %1$d jednorázový balíček.</item> + <item quantity="few">K dispozici jsou %1$d jednorázové balíčky.</item> + <item quantity="many">K dispozici je %1$d jednorázového balíčku.</item> + <item quantity="other">K dispozici je %1$d jednorázových balíčků.</item> + </plurals> + <string name="cordn_keypackages_topup_note">Jakmile zde poprvé něco zveřejníte, Amethyst bude spotřebované balíčky potichu nahrazovat. Ten první ale nikdy nezveřejní za vás.</string> + <string name="cordn_keypackages_withdraw_all">Stáhnout vše</string> + <string name="cordn_keypackages_withdraw_orphans">Stáhnout tyto</string> + <string name="cordn_link_request">Požádat o připojení k této skupině</string> + <string name="cordn_link_request_disclosure">Tímto zveřejníte balíček klíčů pod vlastním klíčem svého účtu a sdělíte koordinátorovi, že se chcete připojit k této skupině — bez ohledu na to, zda někdo odpoví.</string> + <string name="cordn_link_request_failed">O připojení se nepodařilo požádat.</string> + <string name="cordn_link_request_sent">Požádáno. Některý člen skupiny vás musí přidat; pozvánka se pak objeví mezi pozvánkami cordn.</string> + <string name="cordn_link_scan">Skenovat</string> + <string name="cordn_media_download_failed">Tento soubor se nepodařilo otevřít.</string> + <string name="cordn_media_no_server">Pro tento účet není nastaven žádný mediální server, takže soubor nemá kam uložit. Vyberte ho v Nastavení.</string> + <string name="cordn_media_unreadable">Tento soubor nelze přečíst.</string> + <string name="cordn_media_upload_failed">Tento soubor se nepodařilo odeslat.</string> + <plurals name="cordn_member_count"> + <item quantity="one">%1$d člen</item> + <item quantity="few">%1$d členové</item> + <item quantity="many">%1$d člena</item> + <item quantity="other">%1$d členů</item> + </plurals> + <string name="cordn_message_deleted">Zpráva smazána</string> + <string name="cordn_message_details_coordinator">Koordinátor</string> + <string name="cordn_message_details_cursor">Kurzor</string> + <string name="cordn_message_details_sent_at">Odesláno</string> + <string name="cordn_message_details_title">Podrobnosti zprávy</string> + <string name="cordn_message_edited">upraveno</string> + <string name="cordn_pinned_by">Připnul %1$s</string> + <plurals name="cordn_pinned_count"> + <item quantity="one">%1$d připnutá zpráva v této skupině</item> + <item quantity="few">%1$d připnuté zprávy v této skupině</item> + <item quantity="many">%1$d připnuté zprávy v této skupině</item> + <item quantity="other">%1$d připnutých zpráv v této skupině</item> + </plurals> + <string name="cordn_pinned_next">Další připnutá zpráva</string> + <string name="cordn_pinned_previous">Předchozí připnutá zpráva</string> + <string name="cordn_pinned_show_all">Zobrazit všechny připnuté zprávy</string> + <string name="cordn_pinned_title">Připnuté zprávy</string> + <string name="cordn_reactions_title">Reakce</string> + <string name="cordn_requests_accept">Přidat</string> + <string name="cordn_requests_admin_only">Přijmout může jen administrátor. Ve skupině bez administrátorů to je kdokoli.</string> + <string name="cordn_requests_check">Zkontrolovat žádosti</string> + <string name="cordn_requests_decline">Zamítnout</string> + <string name="cordn_requests_failed">Žádosti se nepodařilo načíst.</string> + <string name="cordn_requests_none">Nikdo nečeká na připojení.</string> + <string name="cordn_requests_title">Žádosti o připojení</string> + <string name="cordn_send">Odeslat</string> + <string name="cordn_send_failed">Odeslání se nezdařilo.</string> + <string name="cordn_send_no_session">Koordinátor této skupiny není otevřen, takže zatím nelze nic odeslat.</string> + <string name="cordn_share_copied">Zkopírováno</string> + <string name="cordn_share_copy">Kopírovat odkaz</string> + <string name="cordn_share_explainer">Kdokoli s tímto odkazem může požádat o připojení. Pustit ho dovnitř ale musí administrátor.</string> + <string name="cordn_share_title">Sdílet tuto skupinu</string> + <string name="cordn_voice_play">Přehrát hlasovou zprávu</string> + <string name="cordn_voice_record">Nahrát hlasovou zprávu</string> + <string name="cordn_voice_stop">Zastavit a odeslat</string> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-de-rDE/strings.xml b/commonsUI/src/commonMain/composeResources/values-de-rDE/strings.xml index 64ad7f8fd8..0d938c442c 100644 --- a/commonsUI/src/commonMain/composeResources/values-de-rDE/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-de-rDE/strings.xml @@ -166,6 +166,152 @@ <string name="road_event_traffic_jam">Stau</string> <string name="road_event_unknown">Straßenereignis</string> <!-- NIP-CC geocaching (kinds 37516 / 7516) --> + <string name="geocache_unnamed">Geocache</string> + <string name="geocache_type_traditional">Traditional</string> + <string name="geocache_type_multi">Multi-Cache</string> + <string name="geocache_type_mystery">Mystery</string> + <string name="geocache_size_micro">Mikro</string> + <string name="geocache_size_small">Klein</string> + <string name="geocache_size_regular">Normal</string> + <string name="geocache_size_large">Groß</string> + <string name="geocache_size_other">Andere Größe</string> + <string name="geocache_difficulty_short">D%1$d</string> + <string name="geocache_terrain_short">T%1$d</string> + <string name="geocache_first_to_find">FTF</string> + <string name="geocache_is_art">Kunstwerk</string> + <string name="geocache_mission">Key Quest</string> + <string name="geocache_needs_verification">Verifizierte Funde</string> + <string name="geocache_claimed">FTF vergeben</string> + <string name="geocache_archived">Archiviert</string> + <string name="geocache_hint_tap_to_reveal">Nicht weiter? Tippen, um den Hinweis anzuzeigen</string> + <string name="geocache_hint_other_reading">Nicht lesbar? Für die andere Lesart tippen</string> + <string name="geocache_nearby">In der Nähe</string> + <string name="geocache_found_it">Gefunden!</string> + <string name="geocache_verified_find">Verifizierter Fund</string> + <string name="geocache_invalid_proof">Nachweis ist ungültig</string> + <string name="geocache_loading">Cache wird geladen…</string> + <string name="geocache_hidden_by">Versteckt von %1$s</string> + <string name="geocache_navigate">Navigieren</string> + <string name="geocache_copy_coordinates">Koordinaten kopieren</string> + <string name="geocache_coordinates_copied">Koordinaten kopiert</string> + <string name="geocache_photos_section">Fotos</string> + <string name="geocache_mission_section">Key-Quest-Mission</string> + <string name="geocache_logs_section">Logs</string> + <string name="geocache_no_logs">Diesen Cache hat noch niemand geloggt. Sei der Erste.</string> + <string name="geocache_i_found_it">Ich habe ihn gefunden</string> + <string name="geocache_didnt_find_it">Nicht gefunden</string> + <string name="geocache_add_note">Notiz hinzufügen</string> + <string name="geocache_needs_maintenance">Wartungsbedarf melden</string> + <string name="geocache_add_to_hunt">Zu einer Tour hinzufügen</string> + <string name="geocache_hunt_new">Neue Tour…</string> + <string name="geocache_hunt_none_yet">Du hast noch keine Tour erstellt.</string> + <string name="geocache_hunt_already_on">Bereits in dieser Tour</string> + <plurals name="geocache_caches_here"> + <item quantity="one">%1$d Cache hier</item> + <item quantity="other">%1$d Caches hier</item> + </plurals> + <string name="geocache_photo_remove">Dieses Foto entfernen</string> + <string name="geocache_archived_notice">Dieser Cache wurde archiviert. Er ist nicht mehr im Spiel.</string> + <string name="geocache_ftf_won_by">FTF geht an %1$s</string> + <string name="geocache_ftf_available">Der FTF ist noch zu haben</string> + <string name="geocache_owner_actions">Owner</string> + <string name="geocache_owner_edit">Diesen Cache bearbeiten</string> + <string name="geocache_owner_archive">Diesen Cache archivieren</string> + <string name="geocache_owner_archive_confirm">Archivieren. Finder sehen, dass er nicht mehr im Spiel ist, und das Listing bleibt lesbar.</string> + <string name="geocache_owner_lock_ftf">Erstfinder festschreiben</string> + <string name="geocache_owner_lock_ftf_explain">Veröffentlicht einen F-Tag mit %1$s. Das ist endgültig.</string> + <string name="geocache_owner_no_finders">Noch keine verifizierten Funde, also gibt es niemanden zum Festschreiben.</string> + <string name="geocache_owner_show_qr">Verifizierungscode anzeigen</string> + <string name="geocache_owner_qr_warning">Drucke ihn aus und lege ihn in den Cache. Amethyst speichert ihn nicht — wenn du ihn verlierst, können Finder nicht mehr beweisen, dass sie dort waren.</string> + <string name="geocache_log_type_dnf">Nicht gefunden</string> + <string name="geocache_log_type_note">Notiz</string> + <string name="geocache_log_type_maintenance">Wartung erforderlich</string> + <string name="geocache_log_title">Fund loggen</string> + <string name="geocache_log_cache_label">Cache</string> + <string name="geocache_log_your_log">Dein Log</string> + <string name="geocache_log_placeholder">Wie lief es?</string> + <string name="geocache_log_add_photo">Foto hinzufügen</string> + <string name="geocache_log_mission_answer">Deine Key-Quest-Antwort</string> + <string name="geocache_log_proof_section">Anwesenheitsnachweis</string> + <string name="geocache_log_scan_code">Code im Cache scannen</string> + <string name="geocache_log_scan_again">Anderen Code scannen</string> + <string name="geocache_log_proof_verified">Verifiziert — du warst am Cache</string> + <string name="geocache_log_proof_explain">Einmalig mit dem Code aus der Dose signiert. Der Schlüssel wird nie gespeichert und verlässt diesen Bildschirm nie.</string> + <string name="geocache_log_proof_bad_code">Das ist kein Verifizierungscode.</string> + <string name="geocache_log_proof_wrong_cache">Dieser Code gehört zu einem anderen Cache.</string> + <string name="geocache_log_proof_optional">Dieser Cache verlangt keinen Nachweis, du kannst ihn also einfach loggen.</string> + <string name="geocache_log_no_location_attached">Dein eigener Standort wird diesem Log nicht angehängt.</string> + <string name="geocache_log_post">Senden</string> + <string name="geocache_log_comment_title">Log hinzufügen</string> + <string name="geocache_tab_nearby">In der Nähe</string> + <string name="geocache_tab_map">Karte</string> + <string name="geocache_tab_hunts">Touren</string> + <string name="geocache_tab_finds">Funde</string> + <string name="geocache_tab_mine">Meine</string> + <string name="geocache_filter_unfound">Nicht gefunden</string> + <string name="geocache_filter_difficulty">D ≤ %1$d</string> + <string name="geocache_filter_terrain">T ≤ %1$d</string> + <string name="geocache_filter_has_prize">Mit Preis</string> + <string name="geocache_filter_verified_only">Nachweis erforderlich</string> + <string name="geocache_use_my_location">Meinen Standort verwenden</string> + <string name="geocache_sorted_by_distance">Nach Entfernung sortiert</string> + <string name="geocache_sorted_by_recent">Neueste zuerst</string> + <string name="geocache_hide_a_cache">Cache verstecken</string> + <string name="geocache_empty_nearby">Hier gibt es noch keine Caches. Probiere eine größere Folgeliste oder verstecke den ersten.</string> + <string name="geocache_empty_finds">Du hast noch keinen Fund geloggt.</string> + <string name="geocache_empty_mine">Du hast noch keinen Cache versteckt.</string> + <string name="geocache_empty_hunts">Noch keine Touren.</string> + <string name="geocache_stat_finds">Funde</string> + <string name="geocache_stat_ftf">FTFs</string> + <string name="geocache_stat_verified">Verifiziert</string> + <string name="geocache_stat_hidden">Versteckt</string> + <string name="geocache_map_recenter">Auf mich zentrieren</string> + <string name="geocache_map_long_press">Lange auf die Karte drücken, um dort einen Cache zu verstecken</string> + <string name="geocache_map_open">Öffnen</string> + <string name="geocache_new_section_where">Wo</string> + <string name="geocache_new_section_what">Was</string> + <string name="geocache_new_section_extras">Extras</string> + <string name="geocache_new_section_proof">Nachweis</string> + <string name="geocache_new_section_relays">Relays</string> + <string name="geocache_new_location_warning">Dies veröffentlicht einen öffentlichen Standort mit einer Genauigkeit von etwa 5 Metern. Jeder auf Nostr kann ihn sehen, und ein veröffentlichtes Event kann nicht zurückgeholt werden.</string> + <string name="geocache_new_pick_location">Ort auswählen</string> + <string name="geocache_new_ladder">Veröffentlicht %1$s sowie gröbere Zellen bis hinunter zu 3 Zeichen für die Umkreissuche.</string> + <string name="geocache_new_name">Name</string> + <string name="geocache_new_description">Beschreibung</string> + <string name="geocache_new_type">Typ</string> + <string name="geocache_new_size">Größe</string> + <string name="geocache_new_difficulty">Schwierigkeit</string> + <string name="geocache_new_terrain">Gelände</string> + <string name="geocache_new_hint">Hinweis</string> + <string name="geocache_new_hint_preview">Finder sehen</string> + <string name="geocache_new_hint_scramble">Hinweis verschlüsseln (ROT13)</string> + <string name="geocache_new_mission">Key-Quest-Mission</string> + <string name="geocache_new_modifiers">Zusätze</string> + <string name="geocache_new_require_proof">Anwesenheitsnachweis verlangen</string> + <string name="geocache_new_proof_ready">Für diesen Cache wurde ein Verifizierungsschlüssel erzeugt.</string> + <string name="geocache_new_log_relays">Wo Finder loggen sollen</string> + <string name="geocache_new_publish">Veröffentlichen</string> + <string name="geocache_new_needs_name">Gib dem Cache einen Namen.</string> + <string name="geocache_new_needs_location">Wähle aus, wo der Cache versteckt ist.</string> + <string name="geocache_hunt_progress">%1$d von %2$d gefunden</string> + <string name="geocache_hunt_start">Tour starten</string> + <plurals name="geocache_hunt_caches"> + <item quantity="one">%1$d Cache</item> + <item quantity="other">%1$d Caches</item> + </plurals> + <string name="geocache_hunt_title">Titel</string> + <string name="geocache_hunt_description">Beschreibung</string> + <string name="geocache_hunt_banner">Bannerbild</string> + <string name="geocache_hunt_pick_caches">Caches, in Reihenfolge</string> + <string name="geocache_hunt_add_cache">Cache hinzufügen</string> + <string name="geocache_hunt_move_up">Nach oben</string> + <string name="geocache_hunt_move_down">Nach unten</string> + <string name="geocache_hunt_remove">Entfernen</string> + <string name="geocache_hunt_theme">Seitendesign</string> + <string name="geocache_hunt_map_style">Kartenstil</string> + <string name="geocache_hunt_needs_title">Gib der Tour einen Titel.</string> + <string name="geocache_hunt_needs_caches">Füge mindestens einen Cache hinzu.</string> + <string name="geocache_publish_failed">Veröffentlichen fehlgeschlagen. Prüfe deine Relay-Verbindung und versuche es erneut.</string> <string name="podcast_value_zap_split_hint">Zaps hierauf werden aufgeteilt zwischen:</string> <string name="podcast_value_split_percent">%1$d%%</string> <string name="podcast_value_for_value">Value-for-Value</string> @@ -188,6 +334,51 @@ </plurals> <string name="profile_banner">Profilbanner</string> <string name="point_to_the_qr_code">Auf den QR-Code verweisen</string> + <string name="qr_scanner_camera_rationale">Amethyst braucht die Kamera, um QR-Codes zu scannen. Es wird nichts aufgezeichnet oder hochgeladen — die Bilder werden auf dem Gerät ausgewertet und verworfen.</string> + <string name="qr_scanner_grant_camera">Kamera erlauben</string> + <string name="qr_scanner_camera_blocked">Der Kamerazugriff ist für Amethyst deaktiviert. Du kannst ihn in den Android-Einstellungen wieder aktivieren.</string> + <string name="qr_scanner_open_settings">Einstellungen öffnen</string> + <string name="qr_scanner_unavailable">Der QR-Decoder konnte auf diesem Gerät nicht starten. Du kannst den Code trotzdem als Text einfügen.</string> + <string name="qr_scanner_torch_on">Licht einschalten</string> + <string name="qr_scanner_torch_off">Licht ausschalten</string> + <string name="qr_scanner_dark_hint">Es ist dunkel — probier das Licht</string> + <string name="qr_scanner_scan_image">Bild scannen</string> + <string name="qr_scanner_paste">Einfügen</string> + <string name="qr_scanner_no_code_in_image">Kein QR-Code in diesem Bild gefunden</string> + <string name="qr_scanner_clipboard_empty">Nichts zum Scannen in der Zwischenablage</string> + <string name="qr_scanner_pick_one">Tippe auf den gewünschten Code</string> + <plurals name="qr_scanner_sequence_progress"> + <item quantity="one">%1$s von %2$s Teil erfasst — weiter so</item> + <item quantity="other">%1$s von %2$s Teilen erfasst — weiter so</item> + </plurals> + <string name="qr_scanner_zoom_reset">Zoom zurücksetzen</string> + <string name="qr_scanner_unsupported_title">Amethyst kann diesen Code nicht öffnen</string> + <string name="qr_scanner_unsupported_secret">Dieser Code enthält einen privaten Schlüssel oder ein Kopplungsgeheimnis, daher wird sein Inhalt hier nicht angezeigt. Verwende den Bildschirm, der ihn erwartet.</string> + <string name="qr_scanner_kind_nsec">Das ist ein privater Schlüssel. Füge ihn stattdessen auf dem Anmeldebildschirm ein.</string> + <string name="qr_scanner_kind_wallet">Das ist eine Wallet-Verbindung. Füge sie in den Wallet-Einstellungen hinzu.</string> + <string name="qr_scanner_kind_bunker">Das ist eine Remote-Signer-Adresse. Amethyst gibt solche Adressen heraus, damit andere Apps es um Signaturen bitten können — anmelden kann es sich damit nicht.</string> + <string name="qr_scanner_kind_signer">Das ist eine App, die sich mit deinem Signierer verbinden möchte. Öffne sie über den NIP-46-Signierer-Bildschirm.</string> + <string name="qr_scanner_kind_lightning">Das ist eine Lightning-Rechnung.</string> + <string name="qr_scanner_kind_cashu">Das ist ein Cashu-Token.</string> + <string name="qr_scanner_kind_web">Das ist ein Weblink.</string> + <string name="qr_scanner_kind_text">Das ist einfacher Text, kein Nostr-Code.</string> + <string name="qr_scanner_kind_nostr_unsupported">Das ist ein Nostr-Code, aber keiner, den dieser Bildschirm öffnen kann.</string> + <string name="qr_scanner_label_profile">Profil</string> + <string name="qr_scanner_label_note">Beitrag</string> + <string name="qr_scanner_label_nostr">Nostr-Code</string> + <string name="qr_scanner_label_relay">Relay</string> + <string name="qr_scanner_label_nsec">Privater Schlüssel</string> + <string name="qr_scanner_label_wallet">Wallet-Verbindung</string> + <string name="qr_scanner_label_bunker">Remote-Signer-Adresse</string> + <string name="qr_scanner_label_signer">Verbindungsanfrage an Signierer</string> + <string name="qr_scanner_label_lightning">Lightning-Rechnung</string> + <string name="qr_scanner_label_cashu">Cashu-Token</string> + <string name="qr_scanner_label_web">Weblink</string> + <string name="qr_scanner_label_text">Einfacher Text</string> + <string name="qr_scanner_open_link">Link öffnen</string> + <string name="qr_scanner_copy">Kopieren</string> + <string name="qr_scanner_copied">Kopiert</string> + <string name="qr_scanner_try_again">Erneut scannen</string> <string name="show_qr">QR-Code anzeigen</string> <string name="profile_image">Profilbild</string> <string name="your_profile_image">Dein Profilbild</string> @@ -638,17 +829,70 @@ <string name="workout_hours">Stunden</string> <string name="workout_minutes">Minuten</string> <string name="workout_seconds">Sekunden</string> + <string name="workout_suggestion_connect_title">Nutze die Trainings, die du schon aufgezeichnet hast</string> + <string name="workout_suggestion_connect_message">Verbinde Health Connect (Samsung Health, Google Fit, Fitbit, Garmin…), um „Meine Fitness“ aufzubauen, deine persönliche Trainingsübersicht, und um dieses Training aus einem zu übernehmen, das deine Uhr bereits aufgezeichnet hat, statt jede Zahl von Hand einzutippen.</string> <string name="workout_suggestion_connect_button">Verbinden</string> <string name="workout_suggestion_distance_km">%1$s km</string> <string name="workout_from_health_connect">Aus Health Connect</string> <string name="workout_suggestion_connect_details">Was Amethyst liest</string> <string name="health_connect_rationale_title">Health Connect und Amethyst</string> + <string name="health_connect_rationale_headline">Amethyst nutzt Health Connect, um „Meine Fitness“ aufzubauen — deine persönliche Trainingsübersicht, auf deinem Telefon.</string> + <string name="health_connect_rationale_intro">„Meine Fitness“ (Menü → Du → Meine Fitness) zeigt dir, wie du trainiert hast: wie viel du diese Woche im Vergleich zur letzten gemacht hast, wie sich deine Zeit auf Laufen, Radfahren, Gehen, Schwimmen und Fitnessstudio verteilt, deine Bestleistungen, an wie vielen Tagen du trainiert hast und deine aktuelle Serie. Amethyst erstellt dieses Bild aus den Trainings, die deine Uhr oder Fitness-App bereits in Health Connect gespeichert hat, zusammen mit allen Trainings, die du in Amethyst selbst erfasst hast. Es wird auf deinem Telefon berechnet und nur dir angezeigt.</string> <string name="health_connect_rationale_what_title">Was Amethyst liest und warum</string> + <string name="health_connect_rationale_exercise">Training · welche Aktivität du gemacht hast, wann sie begann und wie lange sie dauerte. Das ist das Training selbst: Daraus ergeben sich deine Anzahl an Trainings, deine Trainingszeit, die Aufschlüsselung nach Aktivität, deine aktiven Tage und deine Serie.</string> + <string name="health_connect_rationale_distance">Distanz · wie weit du gekommen bist, angezeigt als deine Distanz dieser Woche im Vergleich zur letzten, dein Wochendurchschnitt, deine Distanz pro Aktivität und deine längste Distanz.</string> + <string name="health_connect_rationale_calories">Aktive und Gesamtkalorien · die Energie, die ein Training verbrannt hat, angezeigt als deine Energie dieser Woche und im Vergleich zur letzten Woche. Aktive Kalorien werden verwendet, wenn deine Quelle sie erfasst; Gesamtkalorien dienen als Ersatz für Quellen, die nur den Gesamtenergieverbrauch erfassen.</string> + <string name="health_connect_rationale_heart_rate">Herzfrequenz · Durchschnitt und Maximum jedes Trainings, zusammengefasst zu deiner durchschnittlichen und höchsten Herzfrequenz im Zeitraum. So unterscheidest du eine lockere Woche von einer harten bei gleicher Distanz.</string> + <string name="health_connect_rationale_steps">Schritte · die Schrittzahl eines Laufs, Spaziergangs oder einer Wanderung, angezeigt als dein wöchentlicher Schrittdurchschnitt und deine höchste Schrittzahl.</string> + <string name="health_connect_rationale_elevation">Höhenmeter · wie viel du aufgestiegen bist, angezeigt als dein wöchentlicher Durchschnitt an Höhenmetern und dein größter Anstieg. Daran erkennst du eine flache Woche von einer hügeligen.</string> <string name="health_connect_rationale_limits_title">Was Amethyst nicht tut</string> + <string name="health_connect_rationale_limit_window">Liest nur Trainings der letzten 4 Wochen, und nur solange „Meine Fitness“ oder der Trainings-Editor geöffnet ist. Liest nie im Hintergrund.</string> <string name="health_connect_rationale_limit_write">Schreibt nie etwas in Health Connect und fragt nie nach deiner Trainingsroute, deinem Standort oder anderen Gesundheitsdaten.</string> + <string name="health_connect_rationale_limit_publish">Sendet deine Gesundheitsdaten nirgendwohin. „Meine Fitness“ wird auf deinem Telefon berechnet und bleibt dort. Wenn du später ein Training posten möchtest, tippst du bei diesem Training auf Teilen, liest den Beitrag, den Amethyst ausgefüllt hat, und veröffentlichst ihn selbst — ein Training nach dem anderen, nie automatisch.</string> + <string name="health_connect_rationale_limit_optional">Die gesamte Funktion ist optional. Widerrufe die Berechtigungen jederzeit in Health Connect — „Meine Fitness“ zeigt dann wieder die Aufforderung — oder schalte die Editor-Vorschläge unter Einstellungen → Editor-Einstellungen aus. Der Rest von Amethyst funktioniert weiter.</string> <string name="health_connect_rationale_privacy_policy">Vollständige Datenschutzerklärung lesen</string> + <string name="my_fitness_title">Meine Fitness</string> + <string name="my_fitness_window">Letzte 4 Wochen</string> + <string name="my_fitness_loading_metrics">Distanz, Kalorien und Herzfrequenz werden geladen…</string> + <string name="my_fitness_window_note">Die letzten vier vollen Wochen deines Trainings: die Trainings, die du veröffentlicht hast, plus alles, was Health Connect auf diesem Telefon erfasst. Amethyst berechnet die Übersicht auf dem Gerät und veröffentlicht nichts davon.</string> + <string name="my_fitness_this_week">Diese Woche</string> + <string name="my_fitness_vs_last_week">ggü. letzter Woche</string> + <string name="my_fitness_weekly_average">Wochendurchschnitt</string> + <string name="my_fitness_workouts">Trainings</string> + <string name="my_fitness_active_days">Aktive Tage</string> + <string name="my_fitness_streak">Tage in Folge</string> + <string name="my_fitness_time">Zeit</string> <!-- Workout duration totals. %1$d is hours, %2$d is minutes; reorder and re-abbreviate freely — these are English abbreviations for hour/minute/second. --> + <string name="my_fitness_duration_hours_minutes">%1$d Std. %2$d Min.</string> + <string name="my_fitness_duration_minutes">%1$d Min.</string> + <string name="my_fitness_duration_seconds">%1$d Sek.</string> + <string name="my_fitness_distance">Distanz</string> + <string name="my_fitness_calories">Kalorien</string> + <string name="my_fitness_steps">Schritte</string> + <string name="my_fitness_elevation">Höhenmeter</string> + <string name="my_fitness_avg_heart_rate">Ø HF</string> + <string name="my_fitness_max_heart_rate">Max. HF</string> + <string name="my_fitness_by_activity">Nach Aktivität</string> + <string name="my_fitness_bests">Bestleistungen</string> + <string name="my_fitness_best_longest_distance">Längste Distanz</string> + <string name="my_fitness_best_longest_duration">Längstes Training</string> + <string name="my_fitness_best_biggest_climb">Größter Anstieg</string> + <string name="my_fitness_best_most_steps">Meiste Schritte</string> + <string name="my_fitness_best_highest_heart_rate">Höchste Herzfrequenz</string> + <string name="my_fitness_recent">Letzte Trainings</string> + <string name="my_fitness_empty">In den letzten 4 Wochen wurde nichts erfasst. Trainings, die du postest, und alle, die deine Uhr aufzeichnet, erscheinen hier.</string> + <string name="my_fitness_connect_title">Verfolge dein Training</string> + <string name="my_fitness_connect_message">Verbinde Health Connect, um deine Wochensummen, den Trend im Wochenvergleich, deine Bestleistungen und deine Trainingsserie zu sehen. Alles bleibt auf deinem Telefon.</string> + <string name="my_fitness_connect_banner">Verbinde Health Connect, um die Trainings deiner Uhr mit Herzfrequenz, Schritten und Höhenmetern hinzuzufügen.</string> + <string name="my_fitness_connect_button">Verbinden</string> + <string name="my_fitness_share">Dieses Training teilen</string> + <string name="my_fitness_unit_bpm">S/min</string> + <string name="my_fitness_unit_kcal">kcal</string> + <string name="my_fitness_unit_km">km</string> + <string name="my_fitness_unit_mi">mi</string> + <string name="my_fitness_unit_m">m</string> + <string name="my_fitness_unit_ft">ft</string> <string name="nsite_none_found">Noch keine nSites gefunden.</string> <string name="browser_reload">Neu laden</string> <string name="browser_unsupported">Der In-App-Browser benötigt Android 11 oder neuer.</string> @@ -1084,6 +1328,8 @@ <string name="poll_closing_date_time">Schließdatum und -zeit</string> <string name="poll_closing_in">Umfrage endet in %1$s</string> <string name="looking_for_event">Veranstaltung suchen %1$s</string> + <string name="looking_for_event_title">Wird geladen</string> + <string name="looking_for_private_event">Warte, bis deine privaten Nachrichten geladen sind…</string> <string name="send_zap">Zap senden</string> <string name="custom_zaps_add_a_message">Füge eine öffentliche Nachricht hinzu</string> <string name="custom_zaps_add_a_message_private">Füge eine private Nachricht hinzu</string> @@ -1223,6 +1469,8 @@ <!-- Elapsed call time. %1$d is minutes and %2$d seconds in the short form; in the long form %1$d is hours, %2$d minutes and %3$d seconds. Separator and field order are the translator's to change. --> + <string name="call_duration_minutes_seconds">%1$02d:%2$02d</string> + <string name="call_duration_hours_minutes_seconds">%1$d:%2$02d:%3$02d</string> <string name="call_dismiss">Verwerfen</string> <string name="call_switch_camera">Kamera wechseln</string> <string name="call_voice">Sprachanruf</string> @@ -1357,6 +1605,14 @@ <string name="user_preferences">Benutzereinstellungen</string> <string name="settings_search_placeholder">Einstellungen durchsuchen</string> <string name="settings_search_no_results">Keine Einstellungen für „%1$s“ gefunden</string> + <string name="marmot_preview_group_updated">Gruppe aktualisiert</string> + <string name="marmot_preview_media">Anhang</string> + <string name="marmot_preview_no_text">Nachricht</string> + <string name="marmot_preview_with_sender">%1$s: %2$s</string> + <string name="marmot_empty_create_action">Gruppe erstellen</string> + <string name="marmot_invite_device_banner">Marmot-Einladungen gehen an ein anderes Gerät und können hier nicht geöffnet werden.</string> + <string name="marmot_invite_device_banner_dismiss">Schließen</string> + <string name="marmot_invite_device_publish">Von diesem Gerät veröffentlichen</string> <string name="reset_marmot_confirm_title">Marmot-Status zurücksetzen?</string> <string name="reset_marmot_confirm_body">Dadurch werden alle Marmot-Gruppenchats, der Nachrichtenverlauf und alle MLS-Schlüssel auf diesem Gerät für das aktuelle Konto dauerhaft gelöscht. Andere Teilnehmer werden nicht benachrichtigt und können dich bis zu ihrem nächsten Commit weiterhin in Gruppen sehen. Dies kann nicht rückgängig gemacht werden. Beim nächsten Synchronisieren der App wird ein neues KeyPackage veröffentlicht.</string> <string name="reset_marmot_confirm_action">Zurücksetzen</string> @@ -1627,6 +1883,7 @@ <string name="wallet_no_transactions_for_filter">Keine Transaktionen entsprechen diesem Filter</string> <string name="wallet_loading">Wird geladen…</string> <string name="wallet_incoming">Empfangen</string> + <string name="wallet_open_zapped_note">Gezappten Beitrag öffnen</string> <string name="wallet_outgoing">Gesendet</string> <string name="wallet_refresh">Aktualisieren</string> <string name="wallet_filter_all">Alle</string> @@ -1943,6 +2200,8 @@ <string name="video_player_settings_action_pip_description">Video in einem schwebenden Fenster abspielen (ausgeblendet, wenn nicht unterstützt)</string> <string name="video_player_settings_action_cast">An Gerät streamen</string> <string name="video_player_settings_action_cast_description">Sendet das Video an einen Chromecast-Empfänger in deinem WLAN (für lokale Dateien ausgeblendet)</string> + <string name="video_player_settings_action_captions">Untertitel</string> + <string name="video_player_settings_action_captions_description">Untertitel ein- oder ausschalten (ausgeblendet, wenn das Video keine hat)</string> <string name="audio_visualizer_settings_description">Wähle die Animation, die beim Abspielen von Audios angezeigt wird.</string> <string name="audio_visualizer_off">Aus</string> <string name="audio_visualizer_bars">Spektrum-Balken</string> @@ -2630,6 +2889,7 @@ <string name="community_rules_violation_wot_gate_failed">Du erfüllst die Web-of-Trust-Anforderungen dieser Community nicht.</string> <string name="community_rules_violation_stale_rules">Das aktuelle Community-Regeldokument wurde als veraltet abgelehnt.</string> <string name="marmot_groups_title">Marmot-Gruppen</string> + <string name="cordn_groups_title">Cordn-Gruppen</string> <string name="marmot_create_group">Gruppe erstellen</string> <string name="marmot_create_group_title">Marmot-Gruppe erstellen</string> <string name="marmot_create_group_footer">Eine neue MLS-Gruppe wird erstellt. Sie können danach Mitglieder hinzufügen.</string> @@ -2921,13 +3181,18 @@ <string name="app_notification_replies_channel_description">Benachrichtigt dich, wenn jemand auf deinen Beitrag antwortet</string> <string name="app_notification_replies_channel_name">Antworten</string> <string name="app_notification_replies_summary">Neue Antworten</string> + <string name="app_notification_reply_failed">Nicht gesendet</string> <string name="app_notification_reply_label">Antworten</string> + <string name="app_notification_reply_retry">Erneut versuchen</string> + <string name="app_notification_reply_sending">Wird gesendet…</string> + <string name="app_notification_reply_unconfirmed">Wird noch gesendet…</string> <string name="app_notification_reposts_channel_description">Benachrichtigt dich, wenn jemand deinen Beitrag repostet</string> <string name="app_notification_reposts_channel_message">%1$s hat deinen Beitrag repostet</string> <string name="app_notification_reposts_channel_name">Reposts</string> <string name="app_notification_reposts_summary">Neue Reposts</string> <string name="app_notification_scheduled_posts_channel_description">Benachrichtigungen, wenn ein geplanter Beitrag veröffentlicht wird oder die Veröffentlichung fehlschlägt.</string> <string name="app_notification_scheduled_posts_channel_name">Geplante Beiträge</string> + <string name="app_notification_shortcut_account_removed">Dieses Konto wurde aus Amethyst entfernt.</string> <string name="app_notification_zaps_channel_description">Benachrichtigt dich, wenn dich jemand zappt</string> <string name="app_notification_zaps_channel_message">%1$s Sats</string> <string name="app_notification_zaps_channel_message_for">für %1$s</string> @@ -3128,6 +3393,10 @@ <string name="call_unmute">Stummschaltung aufheben</string> <string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration_with_user">Callback-URL aus %1$s Antwort nicht gefunden</string> <string name="cancel">Abbrechen</string> + <string name="captions_off">Aus</string> + <string name="captions_turn_off">Untertitel ausschalten</string> + <string name="captions_turn_on">Untertitel einschalten</string> + <string name="captions_unnamed_track">Untertitel</string> <string name="cashu">Cashu-Token</string> <string name="cashu_checking_mint">Mint wird geprüft…</string> <string name="cashu_failed_redemption">Cashu konnte nicht eingelöst werden</string> @@ -3605,10 +3874,12 @@ <string name="kind_adventure_prologue">Abenteuer-Prolog</string> <string name="kind_adventure_reading">Abenteuer-Lesung</string> <string name="kind_adventure_scene">Abenteuer-Szene</string> + <string name="kind_app_curation_set">App-Liste</string> <string name="kind_app_recommendations">App-Empfehlungen</string> <string name="kind_appointment">Termin</string> <string name="kind_apps">Apps</string> <string name="kind_appt_rsvp">RSVP für Termin</string> + <string name="kind_article_curation_set">Artikelliste</string> <string name="kind_audio_header">Audio-Header</string> <string name="kind_audio_track">Audiospur</string> <string name="kind_authored_podcasts">Erstellte Podcasts</string> @@ -3621,6 +3892,7 @@ <string name="kind_blossom_auth">Blossom-Authentifizierung</string> <string name="kind_blossom_servers">Blossom-Server</string> <string name="kind_bookmark_list">Lesezeichenliste</string> + <string name="kind_bookmark_set">Lesezeichen-Set</string> <string name="kind_broadcast_relays">Broadcast-Relays</string> <string name="kind_calendar">Kalender</string> <string name="kind_channel_definition">Kanaldefinition</string> @@ -3658,6 +3930,7 @@ <string name="kind_emoji_packs">Emoji-Pakete</string> <string name="kind_ephemeral_chat">Ephemerer Chat</string> <string name="kind_ephemeral_chatrooms">Ephemere Chaträume</string> + <string name="kind_favorite_algo_feeds">Lieblings-Feeds</string> <string name="kind_favorite_podcasts">Lieblings-Podcasts</string> <string name="kind_favorite_relays">Bevorzugte Relays</string> <string name="kind_file_headers">Datei-Header</string> @@ -3666,28 +3939,36 @@ <string name="kind_follow_packs">Follow-Pakete</string> <string name="kind_geohash_follows">Geohash-Follows</string> <string name="kind_gift_wraps">Geschenkverpackungen</string> + <string name="kind_git_authors">Git-Autoren</string> <string name="kind_git_issue">Git-Issue</string> <string name="kind_git_patch">Git-Patch</string> <string name="kind_git_pr">Pull Request</string> <string name="kind_git_pr_update">PR Update</string> <string name="kind_git_reply">Git-Antwort</string> <string name="kind_git_repo">Git-Repository</string> + <string name="kind_git_repositories">Git-Repositories</string> <string name="kind_git_status_applied">Git-Status: Angewendet</string> <string name="kind_git_status_closed">Git-Status: Geschlossen</string> <string name="kind_git_status_draft">Git-Status: Entwurf</string> <string name="kind_git_status_open">Git-Status: Offen</string> + <string name="kind_good_wiki_authors">Vertrauenswürdige Wiki-Autoren</string> + <string name="kind_good_wiki_relays">Vertrauenswürdige Wiki-Relays</string> <string name="kind_hashtag_follows">Gefolgte Hashtags</string> <string name="kind_highlights">Highlights</string> <string name="kind_http_auth">HTTP-Authentifizierung</string> <string name="kind_index_relay_list">Index-Relay-Liste</string> + <string name="kind_interest_set">Interessen</string> <string name="kind_live_chats">Live-Chats</string> <string name="kind_live_streams">Live-Streams</string> + <string name="kind_media_follows">Medien-Follows</string> + <string name="kind_media_starter_pack">Medien-Starterpaket</string> <string name="kind_medical_data">Medizinische Daten</string> <string name="kind_meeting_room">Besprechungsraum</string> <string name="kind_meeting_space">Besprechungsbereich</string> <string name="kind_music_playlist">Musik-Playlist</string> <string name="kind_music_track">Musiktitel</string> <string name="kind_mute_list">Stummschaltliste</string> + <string name="kind_mute_set_for_kind">Stummgeschaltet für Typ %1$s</string> <string name="kind_named_bookmarks">Benannte Lesezeichen</string> <string name="kind_nests_servers">Nests-Server</string> <string name="kind_nip">NIP</string> @@ -3702,6 +3983,7 @@ <string name="kind_outbox_relays">Outbox-Relays</string> <string name="kind_pay_to">Zahlen an</string> <string name="kind_people_lists">Personenlisten</string> + <string name="kind_picture_curation_set">Bilderliste</string> <string name="kind_pictures">Bilder</string> <string name="kind_pins">Angepinnt</string> <string name="kind_podcast_episode">Podcast-Folge</string> @@ -3728,6 +4010,7 @@ <string name="kind_search_relays">Such-Relays</string> <string name="kind_shorts">Shorts</string> <string name="kind_shorts_repl">Shorts (Antwort)</string> + <string name="kind_simple_groups">Gruppen</string> <string name="kind_torrent_comments">Torrent-Kommentare</string> <string name="kind_torrents">Torrents</string> <string name="kind_trusted_providers">Vertrauenswürdige Anbieter</string> @@ -3736,7 +4019,10 @@ <string name="kind_user_settings">Benutzereinstellungen</string> <string name="kind_user_status">Benutzerstatus</string> <string name="kind_video">Video</string> + <string name="kind_video_collaboration">Video-Zusammenarbeit</string> + <string name="kind_video_list">Videoliste</string> <string name="kind_video_repl">Video (Antwort)</string> + <string name="kind_video_subtitles">Video-Untertitel</string> <string name="kind_voice_msg">Sprachnachricht</string> <string name="kind_voice_reply">Sprachantwort</string> <string name="kind_wake">Aufwachen</string> @@ -3773,6 +4059,8 @@ <string name="location_change_place">Ort auf der Karte ändern</string> <string name="location_channels">Standort-Kanäle</string> <string name="location_pick_on_map">Wähle einen Ort auf der Karte</string> + <string name="login_bunker_not_supported">Amethyst für Android kann sich nicht mit einer bunker://-Adresse anmelden. Es gibt solche Adressen heraus, damit andere Apps es um Signaturen bitten können, aber anmelden kann es sich damit nicht. Verwende eine externe Signierer-App oder Amethyst Desktop.</string> + <string name="login_nostrconnect_not_supported">Das ist eine App, die sich mit deinem Signierer verbinden möchte, keine Anmeldemöglichkeit. Melde dich zuerst an und öffne sie dann über den NIP-46-Signierer-Bildschirm.</string> <string name="login_with_a_private_key_to_be_able_to_boost_posts">Du verwendest einen öffentlichen Schlüssel und öffentliche Schlüssel sind schreibgeschützt. Mit einem privaten Schlüssel anmelden, um Beiträge boosten zu können</string> <string name="login_with_a_private_key_to_be_able_to_follow">Du verwendest einen öffentlichen Schlüssel und öffentliche Schlüssel sind schreibgeschützt. Mit einem privaten Schlüssel anmelden, um folgen zu können</string> <string name="login_with_a_private_key_to_be_able_to_hide_word">Du verwendest einen öffentlichen Schlüssel, und öffentliche Schlüssel sind nur lesbar. Melden Sie sich mit einem privaten Schlüssel an, um ein Wort oder einen Satz auszublenden</string> @@ -3863,6 +4151,7 @@ <string name="mute_notifications">Benachrichtigungen stummschalten</string> <string name="muted_button">Stummgeschaltet. Drücken, um Ton einzuschalten</string> <string name="my_blossom_data">Meine Blossom-Dateien</string> + <string name="my_fitness">Meine Fitness</string> <string name="my_lists">Meine Listen</string> <string name="name_is_required">Ein Name wird benötigt</string> <string name="namecoin_response_time">%1$dms</string> @@ -4126,6 +4415,7 @@ </plurals> <string name="nip46_signer_remote_app">Remote-Signer-App</string> <string name="nip46_signer_title">Remote-Signer</string> + <string name="nip51_list_has_private_members">Diese Liste hat private Mitglieder, die nur ihr Autor sehen kann</string> <plurals name="nip82_assets_count"> <item quantity="one">%1$d Asset gebündelt</item> <item quantity="other">%1$d Assets bündelt</item> @@ -4658,12 +4948,21 @@ <string name="route_calendars">Kalender</string> <string name="route_chess">Schach</string> <string name="route_discover">Entdecken</string> + <string name="route_edit_geocache">Cache bearbeiten</string> + <string name="route_edit_geocache_hunt">Tour bearbeiten</string> + <string name="route_geocache_detail">Cache</string> + <string name="route_geocache_hunt_detail">Tour</string> + <string name="route_geocache_hunts">Cache-Touren</string> + <string name="route_geocaches">Geocaches</string> <string name="route_home">Startseite</string> <string name="route_import_follows">Folgeliste importieren</string> + <string name="route_log_geocache_find">Fund loggen</string> <string name="route_media">Medien</string> <string name="route_messages">Nachrichten</string> <string name="route_music_playlists">Wiedergabelisten</string> <string name="route_music_tracks">Musik</string> + <string name="route_new_geocache">Cache verstecken</string> + <string name="route_new_geocache_hunt">Neue Tour</string> <string name="route_notifications">Benachrichtigungen</string> <string name="route_podcast_episodes">Folgen</string> <string name="route_podcasts">Podcasts</string> @@ -4853,6 +5152,18 @@ <string name="vanish_request_sent">Löschanfrage gesendet</string> <string name="video_codec_h265_description">Bessere Qualität bei kleinerer Dateigröße, aber nicht alle Geräte unterstützen die H.265-Wiedergabe.</string> <string name="video_codec_h265_label">H.265/HEVC-Codec verwenden</string> + <string name="video_collaboration_accepted_on">Hat eine Mitwirkungsnennung angenommen</string> + <string name="video_collaboration_declined_on">Hat eine Mitwirkungsnennung abgelehnt</string> + <string name="video_credit_accepted">Nennung angenommen</string> + <string name="video_credit_audio_from">Audio von</string> + <string name="video_credit_featuring">Mit</string> + <string name="video_credit_inspired_by">Inspiriert von</string> + <string name="video_credit_references">Verweise</string> + <string name="video_curation_set_private_items">Private Liste</string> + <plurals name="video_curation_set_video_count"> + <item quantity="one">%1$d Video</item> + <item quantity="other">%1$d Videos</item> + </plurals> <string name="video_download_has_started_toast">Der Videodownload wurde gestartet…</string> <string name="video_player_settings">Videoplayer-Schaltflächen</string> <string name="video_saved_to_the_gallery">Video in der Galerie des Smartphones gespeichert</string> @@ -4912,165 +5223,18 @@ <string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">Der Lightning-Service des Empfängers unter %1$s ist nicht verfügbar. Er wurde aus der Lightning-Adresse "%2$s" berechnet. Fehler: %3$s. Überprüfen Sie, ob der Server online ist und ob die Lightning-Adresse korrekt ist</string> <string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">Kann keine Blitzrechnung von %1$s erstellen: Element pr nicht im resultierenden JSON gefunden.</string> <string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">Blitzrechnung konnte nicht erstellt werden. Nachricht von %1$s: %2$s</string> + <string name="marmot_invite_device_success">Dieses Gerät empfängt jetzt deine Marmot-Einladungen.</string> + <string name="marmot_invite_device_failure">KeyPackage konnte nicht veröffentlicht werden: %1$s</string> + <string name="marmot_invite_device_rejected">Kein Relay hat das KeyPackage angenommen. Prüfe deine Relay-Einstellungen und versuche es erneut.</string> + <string name="chat_delivery_sending">Wird gesendet…</string> + <string name="chat_delivery_failed">Konnte nicht gesendet werden. Tippe, um es erneut zu versuchen.</string> + <plurals name="marmot_unread_messages"> + <item quantity="one">%1$d neue Nachricht</item> + <item quantity="other">%1$d neue Nachrichten</item> + </plurals> <!-- cordn: groups delivered by an MCP coordinator rather than by relays. The exposure strings state what the coordinator operator learns; see spec/00.md §8 and quartz/plans/2026-09-17-cordn-interop.md. --> - <!-- cordn entries in the settings catalog and the Messages chat-type - list. Here rather than in amethyst/res because both screens read - them through Res. --> - <!-- cordn strings on screens main migrated to Res: the shared inbox row - and the new-conversation picker. --> - <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a - one-shot handoff rather than continuous sync. The exposure strings say - what leaves the device; the fork strings say why the old phone stops. --> - <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> - <string name="geocache_unnamed">Geocache</string> - <string name="geocache_type_traditional">Traditional</string> - <string name="geocache_type_multi">Multi-Cache</string> - <string name="geocache_type_mystery">Mystery</string> - <string name="geocache_size_micro">Mikro</string> - <string name="geocache_size_small">Klein</string> - <string name="geocache_size_regular">Normal</string> - <string name="geocache_size_large">Groß</string> - <string name="geocache_size_other">Andere Größe</string> - <string name="geocache_difficulty_short">D%1$d</string> - <string name="geocache_terrain_short">T%1$d</string> - <string name="geocache_first_to_find">FTF</string> - <string name="geocache_is_art">Kunstwerk</string> - <string name="geocache_mission">Key Quest</string> - <string name="geocache_needs_verification">Verifizierte Funde</string> - <string name="geocache_claimed">FTF vergeben</string> - <string name="geocache_archived">Archiviert</string> - <string name="geocache_hint_tap_to_reveal">Nicht weiter? Tippen, um den Hinweis anzuzeigen</string> - <string name="geocache_hint_other_reading">Nicht lesbar? Für die andere Lesart tippen</string> - <string name="geocache_nearby">In der Nähe</string> - <string name="geocache_found_it">Gefunden!</string> - <string name="geocache_verified_find">Verifizierter Fund</string> - <string name="geocache_invalid_proof">Nachweis ist ungültig</string> - <string name="geocache_loading">Cache wird geladen…</string> - <string name="geocache_hidden_by">Versteckt von %1$s</string> - <string name="geocache_navigate">Navigieren</string> - <string name="geocache_copy_coordinates">Koordinaten kopieren</string> - <string name="geocache_coordinates_copied">Koordinaten kopiert</string> - <string name="geocache_photos_section">Fotos</string> - <string name="geocache_mission_section">Key-Quest-Mission</string> - <string name="geocache_logs_section">Logs</string> - <string name="geocache_no_logs">Diesen Cache hat noch niemand geloggt. Sei der Erste.</string> - <string name="geocache_i_found_it">Ich habe ihn gefunden</string> - <string name="geocache_didnt_find_it">Nicht gefunden</string> - <string name="geocache_add_note">Notiz hinzufügen</string> - <string name="geocache_needs_maintenance">Wartungsbedarf melden</string> - <string name="geocache_add_to_hunt">Zu einer Tour hinzufügen</string> - <string name="geocache_hunt_new">Neue Tour…</string> - <string name="geocache_hunt_none_yet">Du hast noch keine Tour erstellt.</string> - <string name="geocache_hunt_already_on">Bereits in dieser Tour</string> - <plurals name="geocache_caches_here"> - <item quantity="one">%1$d Cache hier</item> - <item quantity="other">%1$d Caches hier</item> - </plurals> - <string name="geocache_photo_remove">Dieses Foto entfernen</string> - <string name="geocache_archived_notice">Dieser Cache wurde archiviert. Er ist nicht mehr im Spiel.</string> - <string name="geocache_ftf_won_by">FTF geht an %1$s</string> - <string name="geocache_ftf_available">Der FTF ist noch zu haben</string> - <string name="geocache_owner_actions">Owner</string> - <string name="geocache_owner_edit">Diesen Cache bearbeiten</string> - <string name="geocache_owner_archive">Diesen Cache archivieren</string> - <string name="geocache_owner_archive_confirm">Archivieren. Finder sehen, dass er nicht mehr im Spiel ist, und das Listing bleibt lesbar.</string> - <string name="geocache_owner_lock_ftf">Erstfinder festschreiben</string> - <string name="geocache_owner_lock_ftf_explain">Veröffentlicht einen F-Tag mit %1$s. Das ist endgültig.</string> - <string name="geocache_owner_no_finders">Noch keine verifizierten Funde, also gibt es niemanden zum Festschreiben.</string> - <string name="geocache_owner_show_qr">Verifizierungscode anzeigen</string> - <string name="geocache_owner_qr_warning">Drucke ihn aus und lege ihn in den Cache. Amethyst speichert ihn nicht — wenn du ihn verlierst, können Finder nicht mehr beweisen, dass sie dort waren.</string> - <string name="geocache_log_type_dnf">Nicht gefunden</string> - <string name="geocache_log_type_note">Notiz</string> - <string name="geocache_log_type_maintenance">Wartung erforderlich</string> - <string name="geocache_log_title">Fund loggen</string> - <string name="geocache_log_cache_label">Cache</string> - <string name="geocache_log_your_log">Dein Log</string> - <string name="geocache_log_placeholder">Wie lief es?</string> - <string name="geocache_log_add_photo">Foto hinzufügen</string> - <string name="geocache_log_mission_answer">Deine Key-Quest-Antwort</string> - <string name="geocache_log_proof_section">Anwesenheitsnachweis</string> - <string name="geocache_log_scan_code">Code im Cache scannen</string> - <string name="geocache_log_scan_again">Anderen Code scannen</string> - <string name="geocache_log_proof_verified">Verifiziert — du warst am Cache</string> - <string name="geocache_log_proof_explain">Einmalig mit dem Code aus der Dose signiert. Der Schlüssel wird nie gespeichert und verlässt diesen Bildschirm nie.</string> - <string name="geocache_log_proof_bad_code">Das ist kein Verifizierungscode.</string> - <string name="geocache_log_proof_wrong_cache">Dieser Code gehört zu einem anderen Cache.</string> - <string name="geocache_log_proof_optional">Dieser Cache verlangt keinen Nachweis, du kannst ihn also einfach loggen.</string> - <string name="geocache_log_no_location_attached">Dein eigener Standort wird diesem Log nicht angehängt.</string> - <string name="geocache_log_post">Senden</string> - <string name="geocache_log_comment_title">Log hinzufügen</string> - <string name="geocache_tab_nearby">In der Nähe</string> - <string name="geocache_tab_map">Karte</string> - <string name="geocache_tab_hunts">Touren</string> - <string name="geocache_tab_finds">Funde</string> - <string name="geocache_tab_mine">Meine</string> - <string name="geocache_filter_unfound">Nicht gefunden</string> - <string name="geocache_filter_difficulty">D ≤ %1$d</string> - <string name="geocache_filter_terrain">T ≤ %1$d</string> - <string name="geocache_filter_has_prize">Mit Preis</string> - <string name="geocache_filter_verified_only">Nachweis erforderlich</string> - <string name="geocache_use_my_location">Meinen Standort verwenden</string> - <string name="geocache_sorted_by_distance">Nach Entfernung sortiert</string> - <string name="geocache_sorted_by_recent">Neueste zuerst</string> - <string name="geocache_hide_a_cache">Cache verstecken</string> - <string name="geocache_empty_nearby">Hier gibt es noch keine Caches. Probiere eine größere Folgeliste oder verstecke den ersten.</string> - <string name="geocache_empty_finds">Du hast noch keinen Fund geloggt.</string> - <string name="geocache_empty_mine">Du hast noch keinen Cache versteckt.</string> - <string name="geocache_empty_hunts">Noch keine Touren.</string> - <string name="geocache_stat_finds">Funde</string> - <string name="geocache_stat_ftf">FTFs</string> - <string name="geocache_stat_verified">Verifiziert</string> - <string name="geocache_stat_hidden">Versteckt</string> - <string name="geocache_map_recenter">Auf mich zentrieren</string> - <string name="geocache_map_long_press">Lange auf die Karte drücken, um dort einen Cache zu verstecken</string> - <string name="geocache_map_open">Öffnen</string> - <string name="geocache_new_section_where">Wo</string> - <string name="geocache_new_section_what">Was</string> - <string name="geocache_new_section_extras">Extras</string> - <string name="geocache_new_section_proof">Nachweis</string> - <string name="geocache_new_section_relays">Relays</string> - <string name="geocache_new_location_warning">Dies veröffentlicht einen öffentlichen Standort mit einer Genauigkeit von etwa 5 Metern. Jeder auf Nostr kann ihn sehen, und ein veröffentlichtes Event kann nicht zurückgeholt werden.</string> - <string name="geocache_new_pick_location">Ort auswählen</string> - <string name="geocache_new_ladder">Veröffentlicht %1$s sowie gröbere Zellen bis hinunter zu 3 Zeichen für die Umkreissuche.</string> - <string name="geocache_new_name">Name</string> - <string name="geocache_new_description">Beschreibung</string> - <string name="geocache_new_type">Typ</string> - <string name="geocache_new_size">Größe</string> - <string name="geocache_new_difficulty">Schwierigkeit</string> - <string name="geocache_new_terrain">Gelände</string> - <string name="geocache_new_hint">Hinweis</string> - <string name="geocache_new_hint_preview">Finder sehen</string> - <string name="geocache_new_hint_scramble">Hinweis verschlüsseln (ROT13)</string> - <string name="geocache_new_mission">Key-Quest-Mission</string> - <string name="geocache_new_modifiers">Zusätze</string> - <string name="geocache_new_require_proof">Anwesenheitsnachweis verlangen</string> - <string name="geocache_new_proof_ready">Für diesen Cache wurde ein Verifizierungsschlüssel erzeugt.</string> - <string name="geocache_new_log_relays">Wo Finder loggen sollen</string> - <string name="geocache_new_publish">Veröffentlichen</string> - <string name="geocache_new_needs_name">Gib dem Cache einen Namen.</string> - <string name="geocache_new_needs_location">Wähle aus, wo der Cache versteckt ist.</string> - <string name="geocache_hunt_progress">%1$d von %2$d gefunden</string> - <string name="geocache_hunt_start">Tour starten</string> - <plurals name="geocache_hunt_caches"> - <item quantity="one">%1$d Cache</item> - <item quantity="other">%1$d Caches</item> - </plurals> - <string name="geocache_hunt_title">Titel</string> - <string name="geocache_hunt_description">Beschreibung</string> - <string name="geocache_hunt_banner">Bannerbild</string> - <string name="geocache_hunt_pick_caches">Caches, in Reihenfolge</string> - <string name="geocache_hunt_add_cache">Cache hinzufügen</string> - <string name="geocache_hunt_move_up">Nach oben</string> - <string name="geocache_hunt_move_down">Nach unten</string> - <string name="geocache_hunt_remove">Entfernen</string> - <string name="geocache_hunt_theme">Seitendesign</string> - <string name="geocache_hunt_map_style">Kartenstil</string> - <string name="geocache_hunt_needs_title">Gib der Tour einen Titel.</string> - <string name="geocache_hunt_needs_caches">Füge mindestens einen Cache hinzu.</string> - <string name="geocache_publish_failed">Veröffentlichen fehlgeschlagen. Prüfe deine Relay-Verbindung und versuche es erneut.</string> - <string name="cordn_groups_title">Cordn-Gruppen</string> <string name="cordn_badge_coordinator">Koordinator-Gruppe</string> <string name="cordn_badge_coordinator_desc">Zugestellt von einem Koordinator, nicht von Relays</string> <string name="cordn_exposure_title">Was dieser Koordinator sehen kann</string> @@ -5108,6 +5272,9 @@ <string name="cordn_link_relays">Erreichbar über</string> <string name="cordn_link_no_coordinator">Dieser Link nennt keinen Koordinator und kann daher nicht allein verwendet werden. Frag die Person, die ihn geschickt hat, welcher Koordinator die Gruppe überträgt.</string> <string name="cordn_link_not_joinable">Durch das Prüfen eines Links trittst du nichts bei, und ein Link allein macht dich nicht zum Mitglied.</string> + <!-- cordn entries in the settings catalog and the Messages chat-type + list. Here rather than in amethyst/res because both screens read + them through Res. --> <string name="cordn_link_title">Cordn-Gruppenlink</string> <string name="cordn_link_search_keywords">cordn koordinator coordinator gruppe link einladung invite mls chat metadaten privatsphäre offenlegung</string> <string name="cordn_coordinators_title">Cordn-Koordinatoren</string> @@ -5116,6 +5283,10 @@ <string name="cordn_keypackages_search_keywords">cordn keypackage schlüsselpaket veröffentlichen einladen einladung last resort koordinator mls</string> <string name="cordn_backup_title">Cordn-Backup</string> <string name="cordn_backup_search_keywords">cordn backup sicherung wiederherstellen export import passphrase gruppen wiederherstellung</string> + <string name="chat_type_cordn_title">Cordn-Gruppen</string> + <string name="chat_type_cordn_desc">Ende-zu-Ende-verschlüsselte Gruppen, die von einem Koordinator deiner Wahl statt von Relays geordnet werden.</string> + <!-- cordn strings on screens main migrated to Res: the shared inbox row + and the new-conversation picker. --> <string name="cordn_group_untitled">Gruppe %1$s</string> <string name="cordn_group_no_messages_yet">Noch keine Nachrichten</string> <string name="cordn_group_via_coordinator">Cordn-Gruppe, übertragen von %1$s</string> @@ -5124,8 +5295,19 @@ <string name="cordn_preview_photo">Foto</string> <string name="cordn_preview_video">Video</string> <string name="cordn_preview_file">Datei</string> + <string name="new_conversation_cordn_title">Cordn-Gruppe</string> + <string name="new_conversation_cordn_tagline">Verschlüsselter Gruppenchat, geordnet von einem Koordinator deiner Wahl.</string> + <string name="new_conversation_cordn_chip">Koordiniert</string> + <string name="new_conversation_cordn_best">Ideal für Teams, die eine verlässliche Reihenfolge der Unterhaltung wollen.</string> + <string name="new_conversation_cordn_cta">Cordn-Gruppe erstellen</string> + <string name="new_conversation_cordn_pro_1">Ende-zu-Ende-verschlüsselt mit MLS — der Koordinator kann nie eine Nachricht lesen.</string> + <string name="new_conversation_cordn_pro_2">Eine gemeinsame Reihenfolge für alle, sodass der Verlauf nicht durch eine falsch gehende Uhr durcheinandergeraten kann.</string> + <string name="new_conversation_cordn_con_1">Der Koordinator erfährt, wer in der Gruppe ist und wann ihr schreibt, auch wenn er nicht lesen kann, was ihr sagt.</string> <string name="cordn_invitations_entry">In eine cordn-Gruppe eingeladen?</string> <string name="cordn_invitations_entry_action">Einladungen ansehen</string> + <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a + one-shot handoff rather than continuous sync. The exposure strings say + what leaves the device; the fork strings say why the old phone stops. --> <string name="cordn_hub_title">Cordn</string> <string name="cordn_hub_explainer">Gruppen, die von einem Koordinator deiner Wahl statt von Relays zugestellt werden.</string> <string name="cordn_hub_link">Gruppenlink prüfen</string> @@ -5175,453 +5357,6 @@ <string name="cordn_migrate_no_server">Lege zuerst einen Medienserver fest — die verschlüsselten Dokumente brauchen einen Ablageort, während das andere Smartphone sie abruft.</string> <string name="cordn_migrate_exposure_title">Was dieses Gerät verlässt</string> <string name="cordn_migrate_exposure_body">Dein Gruppenzustand wird verschlüsselt und auf deinen Medienserver hochgeladen, damit das andere Smartphone ihn abrufen kann. Für diesen Server ist er nicht lesbar, er sieht nur eine Größe und einen Zeitpunkt. Lösche die Blobs danach, wenn nichts zurückbleiben soll.</string> - <string name="cordn_action_delete">Löschen</string> - <string name="cordn_action_edit">Bearbeiten</string> - <string name="cordn_action_editing">Deine Nachricht wird bearbeitet</string> - <string name="cordn_action_go_to_message">Zur Nachricht</string> - <string name="cordn_action_pin">Anheften</string> - <string name="cordn_action_reply">Antworten</string> - <string name="cordn_action_unpin">Loslösen</string> - <string name="cordn_admin_action_failed">Diese Änderung konnte nicht vorgenommen werden.</string> - <string name="cordn_admin_coordinator_silent">Der Koordinator hat nicht geantwortet, daher wurde nichts geändert. Versuche es gleich noch einmal.</string> - <string name="cordn_admin_no_key_package">%1$s hat kein KeyPackage bei diesem Koordinator veröffentlicht und kann daher noch nicht hinzugefügt werden. Bitte die Person, cordn zuerst auf diesem Koordinator zu öffnen.</string> - <string name="cordn_admin_no_self_remove">Du kannst dich nicht selbst aus einer cordn-Gruppe entfernen.</string> - <string name="cordn_admin_no_welcome">%1$s konnte kein Zugang zur Gruppe gegeben werden, daher wurde das Hinzufügen abgebrochen.</string> - <string name="cordn_admin_not_a_member">%1$s ist nicht in dieser Gruppe.</string> - <string name="cordn_admin_someone">Diese Person</string> - <string name="cordn_admin_wrong_key_package">Dieser Koordinator hat ein KeyPackage geliefert, das jemand anderem gehört, daher wurde %1$s nicht hinzugefügt. Es wurde nichts geändert.</string> - <string name="cordn_backup_contents_body">Deine Gruppen-Verschlüsselungsschlüssel, deine KeyPackages und jede Nachricht in diesen Gruppen. Wer die Datei öffnet, kann die Unterhaltungen lesen. Sobald sie Amethyst verlässt, schützt sie nur noch die Passphrase.</string> - <string name="cordn_backup_contents_title">Was in der Datei steckt</string> - <string name="cordn_backup_explainer">Dein nsec kann cordn-Gruppen nicht zurückholen. Eine cordn-Gruppe existiert als Verschlüsselungszustand auf diesem Gerät und als geordneter Datenstrom auf einem Koordinator, der ihn nicht lesen kann — verlierst du das Gerät ohne Backup, ist die Gruppe weg, samt allem, was darin schon gesagt wurde.</string> - <string name="cordn_backup_export">Exportieren</string> - <string name="cordn_backup_exported">Backup gespeichert.</string> - <string name="cordn_backup_failed">Das hat nicht funktioniert. Prüfe die Passphrase und die Datei.</string> - <string name="cordn_backup_passphrase">Passphrase</string> - <string name="cordn_backup_restore">Wiederherstellen</string> - <string name="cordn_backup_restore_body">Beim Wiederherstellen werden die cordn-Gruppen auf diesem Gerät durch die aus der Datei ersetzt. Das ist für ein Smartphone gedacht, das ein anderes abgelöst hat — nicht, um dieselben Gruppen gleichzeitig auf zwei Geräten zu nutzen.</string> - <string name="cordn_backup_restore_confirm_body">Jede cordn-Gruppe, die jetzt auf diesem Gerät ist, wird entfernt und durch die aus der Datei ersetzt.\n\nStelle nicht auf einem zweiten Smartphone wieder her, während das erste noch genutzt wird. Beide hätten dieselben Gruppenschlüssel, und sobald beide eine Nachricht gesendet haben, geht die Gruppe für alle kaputt — unbemerkt und ohne Weg zurück.</string> - <string name="cordn_backup_restore_confirm_title">Die cordn-Gruppen dieses Geräts ersetzen?</string> - <string name="cordn_backup_restore_title">Wiederherstellen</string> - <string name="cordn_backup_restored">Wiederhergestellt.</string> - <string name="cordn_chat_empty_description">Alles, was hier gesendet wird, ist für diese Gruppe verschlüsselt. Der Koordinator leitet es weiter, ohne es lesen zu können.</string> - <string name="cordn_chat_empty_title">Noch keine Nachrichten</string> - <string name="cordn_chat_unread_divider">Neue Nachrichten</string> - <string name="cordn_chat_yesterday">Gestern</string> - <string name="cordn_composer_hint">Nachricht</string> - <string name="cordn_coordinators_add_action">Hinzufügen</string> - <string name="cordn_coordinators_add_failed">Dieser Koordinator konnte nicht geöffnet werden.</string> - <string name="cordn_coordinators_copy_key">npub des Koordinators kopieren</string> - <string name="cordn_coordinators_discover_action">Nach Koordinatoren suchen</string> - <string name="cordn_coordinators_discover_add">Hinzufügen</string> - <string name="cordn_coordinators_discover_explainer">Liest Ankündigungen, die deine Relays ohnehin führen. Kein Koordinator wird kontaktiert, daher erfährt keiner, dass du gesucht hast.</string> - <string name="cordn_coordinators_discover_failed">Ankündigungen konnten nicht gelesen werden</string> - <string name="cordn_coordinators_discover_none">Auf deinen Relays kündigt sich niemand an.</string> - <string name="cordn_coordinators_discover_seen">Vor %1$s angekündigt</string> - <string name="cordn_coordinators_discover_seen_on">Zuletzt angekündigt %1$s</string> - <plurals name="cordn_coordinators_discover_unheard"> - <item quantity="one">Nichts gefunden, und %1$d deiner Relays hat nicht geantwortet.</item> - <item quantity="other">Nichts gefunden, und %1$d deiner Relays haben nicht geantwortet.</item> - </plurals> - <string name="cordn_coordinators_explainer">Ein Koordinator legt die Reihenfolge der Nachrichten einer Gruppe fest und ist der Einzige, der das kann. Geht er verloren, ist die Reihenfolge verloren, und ein zweiter spiegelt den ersten nicht — daher sind das benannte Dienste, die du auswählst, keine austauschbaren Relays.</string> - <plurals name="cordn_coordinators_health_down"> - <item quantity="one">Antwortet nicht — %1$d Anfrage in Folge ist fehlgeschlagen. Gruppen darauf werden nicht synchronisiert.</item> - <item quantity="other">Antwortet nicht — %1$d Anfragen in Folge sind fehlgeschlagen. Gruppen darauf werden nicht synchronisiert.</item> - </plurals> - <string name="cordn_coordinators_health_ok">Antwortet.</string> - <string name="cordn_coordinators_health_retrying">Eine Anfrage ist fehlgeschlagen; neuer Versuch läuft.</string> - <string name="cordn_coordinators_health_unknown">Noch nichts angefragt.</string> - <string name="cordn_coordinators_hide_older">Die ausblenden, die sich nicht mehr ankündigen</string> - <string name="cordn_coordinators_identify">Fragen, wer er ist</string> - <string name="cordn_coordinators_key">Koordinator-Schlüssel</string> - <string name="cordn_coordinators_label">Ein Name dafür (deiner, optional)</string> - <string name="cordn_coordinators_label_note">Dieser Name gehört dir und bleibt auf diesem Gerät. Ein Koordinator kann keinen Namen beweisen, daher wird er nie nach einem gefragt.</string> - <string name="cordn_coordinators_more">Weitere Aktionen</string> - <string name="cordn_coordinators_none">Noch keine Koordinatoren.</string> - <string name="cordn_coordinators_purge">Löschen</string> - <string name="cordn_coordinators_purge_body">Beim Entfernen bleiben die Gruppen auf diesem Gerät, sodass sie zurückkommen, wenn du den Koordinator wieder hinzufügst. Beim Löschen werden sie vernichtet: Die Gruppenschlüssel, die Lesepositionen und deine KeyPackages verschwinden, und diese Gruppen können nur über eine neue Einladung wieder betreten werden.\n\nDer Koordinator wird nicht benachrichtigt und behält alles, was er bereits hatte.</string> - <string name="cordn_coordinators_purge_title">Diesen Koordinator löschen?</string> - <string name="cordn_coordinators_relays">%1$s</string> - <string name="cordn_coordinators_relays_label">Antwortet auf</string> - <string name="cordn_coordinators_relays_more">%1$s +%2$d weitere</string> - <string name="cordn_coordinators_remove">Entfernen</string> - <string name="cordn_coordinators_rename">Umbenennen</string> - <string name="cordn_coordinators_server">Gibt an: %1$s</string> - <string name="cordn_coordinators_server_claim">Name und Version eines Koordinators sind selbst gewählte Angaben. Nur sein öffentlicher Schlüssel identifiziert ihn.</string> - <string name="cordn_coordinators_server_silent">Hat geantwortet, aber nichts angegeben.</string> - <string name="cordn_coordinators_show_all">Alle %1$d anzeigen</string> - <string name="cordn_coordinators_show_fewer">Weniger anzeigen</string> - <plurals name="cordn_coordinators_show_older"> - <item quantity="one">%1$d anzeigen, der sich nicht mehr ankündigt</item> - <item quantity="other">%1$d anzeigen, die sich nicht mehr ankündigen</item> - </plurals> - <string name="cordn_coordinators_stale_note">Diese haben sich seit über einem Monat nicht angekündigt. Eine Gruppe auf einem verschwundenen Koordinator zu erstellen, schlägt fehl.</string> - <string name="cordn_create_action">Gruppe erstellen</string> - <plurals name="cordn_create_action_invite"> - <item quantity="one">Erstellen und %1$d Person einladen</item> - <item quantity="other">Erstellen und %1$d Personen einladen</item> - </plurals> - <string name="cordn_create_add_member">Jemanden hinzufügen</string> - <string name="cordn_create_admin">Admin</string> - <plurals name="cordn_create_admin_count"> - <item quantity="one">%1$d Person kann hinzufügen und entfernen</item> - <item quantity="other">%1$d Personen können hinzufügen und entfernen</item> - </plurals> - <string name="cordn_create_admin_only_me">Nur ich verwalte diese Gruppe</string> - <string name="cordn_create_admin_only_me_on">Nur du kannst einladen, Mitglieder entfernen oder die Gruppendetails ändern. Du kannst das später an jemand anderen übergeben.</string> - <string name="cordn_create_coordinator">Koordinator</string> - <string name="cordn_create_coordinator_change">Ändern</string> - <string name="cordn_create_coordinator_discover">Koordinatoren finden</string> - <string name="cordn_create_coordinator_hide">Fertig</string> - <string name="cordn_create_coordinator_new">Anderer Koordinator</string> - <string name="cordn_create_coordinator_pubkey">Öffentlicher Schlüssel des Koordinators (hex oder npub)</string> - <string name="cordn_create_coordinator_relays">Relays, auf denen er antwortet, eines pro Zeile</string> - <plurals name="cordn_create_coverage_all"> - <item quantity="one">Erreicht deine %1$d Person</item> - <item quantity="other">Erreicht alle %1$d deiner Personen</item> - </plurals> - <string name="cordn_create_coverage_partial">Erreicht %1$d von %2$d — die übrigen blieben außen vor</string> - <string name="cordn_create_coverage_unknown">Nicht geprüft — bei Auswahl wird nachgefragt</string> - <string name="cordn_create_description">Beschreibung (optional)</string> - <string name="cordn_create_egalitarian">Alle können hinzufügen und entfernen</string> - <string name="cordn_create_egalitarian_note">Alle in der Gruppe können einladen, Mitglieder entfernen und die Details ändern. Jedes Mitglied kann später Admins ernennen, womit das endet.</string> - <string name="cordn_create_explainer">Eine cordn-Gruppe wird von einem Koordinator deiner Wahl geordnet. Die Gruppe wird auf diesem Gerät erstellt — der Koordinator erfährt erst von ihr, wenn du jemanden einlädst oder eine Nachricht sendest.</string> - <string name="cordn_create_failed">Die Gruppe konnte nicht erstellt werden.</string> - <string name="cordn_create_name">Gruppenname</string> - <string name="cordn_create_no_coordinator">Kein Koordinator ausgewählt</string> - <string name="cordn_create_outcome_explainer">Jede Einladung ist eine Welcome-Nachricht, die beim Koordinator hinterlegt wird, daher können sie einzeln fehlschlagen. Niemand wurde benachrichtigt: Die Eingeladenen sehen sie, wenn sie ihre App das nächste Mal öffnen.</string> - <string name="cordn_create_outcome_failed">Der Koordinator hat die Einladung abgelehnt</string> - <string name="cordn_create_outcome_link">Link senden</string> - <string name="cordn_create_outcome_no_welcome">Zur Gruppe hinzugefügt, aber die Einladung hat die Person nicht erreicht — versuche es auf dem Gruppenbildschirm erneut</string> - <string name="cordn_create_outcome_open">Chat öffnen</string> - <string name="cordn_create_outcome_skipped">Kein Schlüssel hier, daher konnte keine Welcome-Nachricht hinterlegt werden</string> - <string name="cordn_create_outcome_title">Die Gruppe existiert</string> - <string name="cordn_create_outcome_waiting">Warte auf die Person</string> - <string name="cordn_create_people_done">Fertig</string> - <string name="cordn_create_people_empty">Noch niemand hinzugefügt. Eine Gruppe kann leer erstellt und Einladungen später gesendet werden, aber wenn du jetzt Personen nennst, zeigt sich, welcher Koordinator sie erreichen kann.</string> - <string name="cordn_create_people_none">Noch niemand — tippen, um Personen hinzuzufügen</string> - <string name="cordn_create_people_note">Wenn du jetzt Personen nennst, erfahren wir, welcher Koordinator sie erreichen kann. Jede erhält eine Welcome-Nachricht auf diesem Koordinator, die sie abholt, wenn sie ihre App das nächste Mal öffnet.</string> - <plurals name="cordn_create_reach_count"> - <item quantity="one">Erreichbar über %1$d Koordinator</item> - <item quantity="other">Erreichbar über %1$d Koordinatoren</item> - </plurals> - <string name="cordn_create_reach_none">Kein Schlüssel auf einem deiner Koordinatoren</string> - <string name="cordn_create_reach_unknown">Noch nicht geprüft</string> - <string name="cordn_create_step_admins">Wer hinzufügen und entfernen kann</string> - <string name="cordn_create_step_name">Name</string> - <string name="cordn_create_step_people">Wer dabei ist</string> - <string name="cordn_create_step_where">Wo sie liegt</string> - <string name="cordn_create_title">Neue Cordn-Gruppe</string> - <plurals name="cordn_create_unreachable_count"> - <item quantity="one">%1$d Person ist über keinen deiner Koordinatoren erreichbar</item> - <item quantity="other">%1$d Personen sind über keinen deiner Koordinatoren erreichbar</item> - </plurals> - <string name="cordn_create_you">Du, immer Admin</string> - <string name="cordn_delete_confirm_body">Alle in der Gruppe sehen diese Nachricht als zurückgezogen, und Amethyst zeigt ihren Text nicht mehr an. Der Koordinator behält die verschlüsselte Kopie, als die sie gesendet wurde, und wer sie schon gelesen hat, hat sie gelesen.</string> - <string name="cordn_delete_confirm_title">Nachricht zurückziehen</string> - <string name="cordn_delivery_accepted">Vom Koordinator angenommen</string> - <string name="cordn_exposure_close">Verstanden</string> - <string name="cordn_exposure_open">Was dieser Koordinator sehen kann</string> - <string name="cordn_group_info">Gruppeninfo</string> - <string name="cordn_group_unavailable">Diese Gruppe ist nicht verfügbar. Ihr Koordinator wurde möglicherweise entfernt.</string> - <string name="cordn_group_unavailable_detail">Füge in den Einstellungen einen Koordinator hinzu, um cordn-Gruppen zu nutzen.</string> - <plurals name="cordn_groups_count"> - <item quantity="one">%1$d Gruppe</item> - <item quantity="other">%1$d Gruppen</item> - </plurals> - <string name="cordn_groups_down_note">Aus diesen Gruppen kommt nichts an. Amethyst versucht es weiter.</string> - <string name="cordn_groups_manage">Koordinatoren</string> - <string name="cordn_groups_none">Noch keine cordn-Gruppen</string> - <string name="cordn_groups_none_detail">Eine cordn-Gruppe liegt auf einem Koordinator, der ihre Mitgliedschaft verwaltet und Nachrichten weitergibt, ohne je zu sehen, was jemand schreibt.</string> - <string name="cordn_groups_start">Eine starten</string> - <string name="cordn_info_add_member">Jemanden hinzufügen</string> - <string name="cordn_info_add_member_none">Niemand mit diesem Namen gefunden.</string> - <string name="cordn_info_add_member_note">Personen können nur hinzugefügt werden, wenn sie ein KeyPackage bei diesem Koordinator veröffentlicht haben.</string> - <string name="cordn_info_add_member_placeholder">Name, npub oder name@domain</string> - <string name="cordn_info_admin_badge">Admin</string> - <plurals name="cordn_info_admins_kept"> - <item quantity="one">Der %1$d Admin der Gruppe bleibt unverändert.</item> - <item quantity="other">Die %1$d Admins der Gruppe bleiben unverändert.</item> - </plurals> - <string name="cordn_info_coordinator">Koordinator</string> - <string name="cordn_info_coordinator_key">Koordinator-Schlüssel</string> - <string name="cordn_info_coordinator_nprofile">Koordinator-Link</string> - <string name="cordn_info_copy_nprofile">nprofile des Koordinators kopieren</string> - <string name="cordn_info_edit_details">Details bearbeiten</string> - <string name="cordn_info_egalitarian">Diese Gruppe hat keine Admins: Alle können dauerhaft hinzufügen, entfernen und umbenennen.</string> - <string name="cordn_info_epoch">Epoche</string> - <string name="cordn_info_gid">Gruppen-ID</string> - <string name="cordn_info_has_key_package">Kann über diesen Koordinator hinzugefügt werden</string> - <string name="cordn_info_members">Mitglieder</string> - <string name="cordn_info_remove_confirm_body">%1$s erhält von hier keine Nachrichten mehr. Bereits Gelesenes bleibt erhalten, und das lässt sich nicht rückgängig machen — die Person müsste erneut eingeladen werden.</string> - <string name="cordn_info_remove_confirm_title">Aus der Gruppe entfernen?</string> - <string name="cordn_info_remove_member">Entfernen</string> - <string name="cordn_info_save">Speichern</string> - <string name="cordn_info_technical">Technische Details</string> - <string name="cordn_invitations_accept">Beitreten</string> - <string name="cordn_invitations_decline">Ablehnen</string> - <string name="cordn_invitations_decline_warning">Ablehnen ist endgültig. Um wieder hineinzukommen, musst du erneut eingeladen werden.</string> - <string name="cordn_invitations_explainer">Wird nur geprüft, wenn du diesen Bildschirm öffnest, und sonst nie — einen Koordinator zu fragen, ob dich jemand eingeladen hat, verrät ihm, dass du da bist, daher tut Amethyst das nicht im Hintergrund.</string> - <string name="cordn_invitations_load_failed">Einladungen konnten nicht gelesen werden.</string> - <plurals name="cordn_invitations_members"> - <item quantity="one">%1$d Person ist bereits in dieser Gruppe</item> - <item quantity="other">%1$d Personen sind bereits in dieser Gruppe</item> - </plurals> - <string name="cordn_invitations_members_more">+%1$d weitere</string> - <string name="cordn_invitations_no_coordinators">Du hast keinen Koordinator geöffnet. cordn-Einladungen kommen über einen Koordinator an, daher gibt es noch nirgends etwas nachzusehen.</string> - <string name="cordn_invitations_none">Keine wartenden Einladungen.</string> - <string name="cordn_invitations_refresh">Erneut prüfen</string> - <string name="cordn_invitations_skipped">Eine Einladung wurde für ein anderes Gerät hinterlegt</string> - <string name="cordn_invitations_title">Cordn-Einladungen</string> - <string name="cordn_invitations_unreachable">%1$s hat nicht geantwortet</string> - <string name="cordn_invitations_via">Über %1$s</string> - <string name="cordn_keypackages_disclosure">Beim Veröffentlichen wird ein Eintrag mit deinem eigenen Kontoschlüssel signiert. Dieser Koordinator weiß dann, dass dieses Konto existiert und eingeladen werden kann — dauerhaft, und unabhängig davon, ob dich jemand einlädt.</string> - <string name="cordn_keypackages_explainer">Andere können dich nur zu einer cordn-Gruppe hinzufügen, indem sie ein KeyPackage verwenden, das du bei diesem Koordinator veröffentlicht hast. cordn hat keinen anderen Ort dafür, also kann dich ohne veröffentlichtes KeyPackage niemand einladen — und nichts sagt ihnen, warum.</string> - <string name="cordn_keypackages_failed">Die KeyPackages konnten nicht gelesen werden.</string> - <string name="cordn_keypackages_last_resort_no">Kein Last-Resort-Paket. Sobald die Einmal-Pakete aufgebraucht sind, schlagen Einladungen fehl.</string> - <string name="cordn_keypackages_last_resort_yes">Ein Last-Resort-Paket ist veröffentlicht, daher sind Einladungen auch dann noch möglich, wenn die Einmal-Pakete aufgebraucht sind.</string> - <plurals name="cordn_keypackages_orphans"> - <item quantity="one">%1$d Paket, das dieses Gerät nicht öffnen kann</item> - <item quantity="other">%1$d Pakete, die dieses Gerät nicht öffnen kann</item> - </plurals> - <string name="cordn_keypackages_orphans_body">Der Koordinator gibt diese an jeden weiter, der dich einlädt, aber die private Hälfte ist nicht auf diesem Gerät — sie gehört zu einer anderen Installation. Gibt es diese Installation nicht mehr, erzeugt eine Einladung mit einem dieser Pakete eine Welcome-Nachricht, die niemand je öffnen kann.</string> - <string name="cordn_keypackages_publish">Eines veröffentlichen</string> - <string name="cordn_keypackages_publish_last_resort">Last-Resort veröffentlichen</string> - <plurals name="cordn_keypackages_summary"> - <item quantity="one">%1$d Einmal-Paket verfügbar.</item> - <item quantity="other">%1$d Einmal-Pakete verfügbar.</item> - </plurals> - <string name="cordn_keypackages_topup_note">Sobald du hier veröffentlicht hast, ersetzt Amethyst verbrauchte Pakete unauffällig. Das erste veröffentlicht es nie für dich.</string> - <string name="cordn_keypackages_withdraw_all">Alle zurückziehen</string> - <string name="cordn_keypackages_withdraw_orphans">Diese zurückziehen</string> - <string name="cordn_link_request">Beitritt zu dieser Gruppe anfragen</string> - <string name="cordn_link_request_disclosure">Dies veröffentlicht ein KeyPackage unter deinem eigenen Kontoschlüssel und teilt dem Koordinator mit, dass du in diese Gruppe möchtest — unabhängig davon, ob jemand antwortet.</string> - <string name="cordn_link_request_failed">Beitrittsanfrage fehlgeschlagen.</string> - <string name="cordn_link_request_sent">Angefragt. Ein Mitglied der Gruppe muss dich hinzufügen; die Einladung erscheint unter cordn-Einladungen.</string> - <string name="cordn_link_scan">Scannen</string> - <string name="cordn_media_download_failed">Diese Datei konnte nicht geöffnet werden.</string> - <string name="cordn_media_no_server">Für dieses Konto ist kein Medienserver festgelegt, daher gibt es keinen Ablageort für die Datei. Wähle einen in den Einstellungen.</string> - <string name="cordn_media_unreadable">Diese Datei konnte nicht gelesen werden.</string> - <string name="cordn_media_upload_failed">Diese Datei konnte nicht gesendet werden.</string> - <plurals name="cordn_member_count"> - <item quantity="one">%1$d Mitglied</item> - <item quantity="other">%1$d Mitglieder</item> - </plurals> - <string name="cordn_message_deleted">Nachricht gelöscht</string> - <string name="cordn_message_details_coordinator">Koordinator</string> - <string name="cordn_message_details_cursor">Cursor</string> - <string name="cordn_message_details_sent_at">Gesendet</string> - <string name="cordn_message_details_title">Nachrichtendetails</string> - <string name="cordn_message_edited">bearbeitet</string> - <string name="cordn_pinned_by">Angeheftet von %1$s</string> - <plurals name="cordn_pinned_count"> - <item quantity="one">%1$d angeheftete Nachricht in dieser Gruppe</item> - <item quantity="other">%1$d angeheftete Nachrichten in dieser Gruppe</item> - </plurals> - <string name="cordn_pinned_next">Nächste angeheftete Nachricht</string> - <string name="cordn_pinned_previous">Vorherige angeheftete Nachricht</string> - <string name="cordn_pinned_show_all">Alle angehefteten Nachrichten anzeigen</string> - <string name="cordn_pinned_title">Angeheftete Nachrichten</string> - <string name="cordn_reactions_title">Reaktionen</string> - <string name="cordn_requests_accept">Hinzufügen</string> - <string name="cordn_requests_admin_only">Nur ein Admin kann annehmen. In einer Gruppe ohne Admins sind das alle.</string> - <string name="cordn_requests_check">Nach Anfragen suchen</string> - <string name="cordn_requests_decline">Verwerfen</string> - <string name="cordn_requests_failed">Die Anfragen konnten nicht gelesen werden.</string> - <string name="cordn_requests_none">Niemand wartet auf den Beitritt.</string> - <string name="cordn_requests_title">Beitrittsanfragen</string> - <string name="cordn_send">Senden</string> - <string name="cordn_send_failed">Das wurde nicht gesendet.</string> - <string name="cordn_send_no_session">Der Koordinator dieser Gruppe ist nicht geöffnet, daher kann noch nichts gesendet werden.</string> - <string name="cordn_share_copied">Kopiert</string> - <string name="cordn_share_copy">Link kopieren</string> - <string name="cordn_share_explainer">Jeder mit diesem Link kann den Beitritt anfragen. Ein Admin muss ihn trotzdem noch hereinlassen.</string> - <string name="cordn_share_title">Diese Gruppe teilen</string> - <string name="cordn_voice_play">Sprachnachricht abspielen</string> - <string name="cordn_voice_record">Sprachnachricht aufnehmen</string> - <string name="cordn_voice_stop">Stoppen und senden</string> - <string name="qr_scanner_camera_rationale">Amethyst braucht die Kamera, um QR-Codes zu scannen. Es wird nichts aufgezeichnet oder hochgeladen — die Bilder werden auf dem Gerät ausgewertet und verworfen.</string> - <string name="qr_scanner_grant_camera">Kamera erlauben</string> - <string name="qr_scanner_camera_blocked">Der Kamerazugriff ist für Amethyst deaktiviert. Du kannst ihn in den Android-Einstellungen wieder aktivieren.</string> - <string name="qr_scanner_open_settings">Einstellungen öffnen</string> - <string name="qr_scanner_unavailable">Der QR-Decoder konnte auf diesem Gerät nicht starten. Du kannst den Code trotzdem als Text einfügen.</string> - <string name="qr_scanner_torch_on">Licht einschalten</string> - <string name="qr_scanner_torch_off">Licht ausschalten</string> - <string name="qr_scanner_dark_hint">Es ist dunkel — probier das Licht</string> - <string name="qr_scanner_scan_image">Bild scannen</string> - <string name="qr_scanner_paste">Einfügen</string> - <string name="qr_scanner_no_code_in_image">Kein QR-Code in diesem Bild gefunden</string> - <string name="qr_scanner_clipboard_empty">Nichts zum Scannen in der Zwischenablage</string> - <string name="qr_scanner_pick_one">Tippe auf den gewünschten Code</string> - <plurals name="qr_scanner_sequence_progress"> - <item quantity="one">%1$s von %2$s Teil erfasst — weiter so</item> - <item quantity="other">%1$s von %2$s Teilen erfasst — weiter so</item> - </plurals> - <string name="qr_scanner_zoom_reset">Zoom zurücksetzen</string> - <string name="qr_scanner_unsupported_title">Amethyst kann diesen Code nicht öffnen</string> - <string name="qr_scanner_unsupported_secret">Dieser Code enthält einen privaten Schlüssel oder ein Kopplungsgeheimnis, daher wird sein Inhalt hier nicht angezeigt. Verwende den Bildschirm, der ihn erwartet.</string> - <string name="qr_scanner_kind_nsec">Das ist ein privater Schlüssel. Füge ihn stattdessen auf dem Anmeldebildschirm ein.</string> - <string name="qr_scanner_kind_wallet">Das ist eine Wallet-Verbindung. Füge sie in den Wallet-Einstellungen hinzu.</string> - <string name="qr_scanner_kind_bunker">Das ist eine Remote-Signer-Adresse. Amethyst gibt solche Adressen heraus, damit andere Apps es um Signaturen bitten können — anmelden kann es sich damit nicht.</string> - <string name="qr_scanner_kind_signer">Das ist eine App, die sich mit deinem Signierer verbinden möchte. Öffne sie über den NIP-46-Signierer-Bildschirm.</string> - <string name="qr_scanner_kind_lightning">Das ist eine Lightning-Rechnung.</string> - <string name="qr_scanner_kind_cashu">Das ist ein Cashu-Token.</string> - <string name="qr_scanner_kind_web">Das ist ein Weblink.</string> - <string name="qr_scanner_kind_text">Das ist einfacher Text, kein Nostr-Code.</string> - <string name="qr_scanner_kind_nostr_unsupported">Das ist ein Nostr-Code, aber keiner, den dieser Bildschirm öffnen kann.</string> - <string name="qr_scanner_label_profile">Profil</string> - <string name="qr_scanner_label_note">Beitrag</string> - <string name="qr_scanner_label_nostr">Nostr-Code</string> - <string name="qr_scanner_label_relay">Relay</string> - <string name="qr_scanner_label_nsec">Privater Schlüssel</string> - <string name="qr_scanner_label_wallet">Wallet-Verbindung</string> - <string name="qr_scanner_label_bunker">Remote-Signer-Adresse</string> - <string name="qr_scanner_label_signer">Verbindungsanfrage an Signierer</string> - <string name="qr_scanner_label_lightning">Lightning-Rechnung</string> - <string name="qr_scanner_label_cashu">Cashu-Token</string> - <string name="qr_scanner_label_web">Weblink</string> - <string name="qr_scanner_label_text">Einfacher Text</string> - <string name="qr_scanner_open_link">Link öffnen</string> - <string name="qr_scanner_copy">Kopieren</string> - <string name="qr_scanner_copied">Kopiert</string> - <string name="qr_scanner_try_again">Erneut scannen</string> - <string name="workout_suggestion_connect_title">Nutze die Trainings, die du schon aufgezeichnet hast</string> - <string name="workout_suggestion_connect_message">Verbinde Health Connect (Samsung Health, Google Fit, Fitbit, Garmin…), um „Meine Fitness“ aufzubauen, deine persönliche Trainingsübersicht, und um dieses Training aus einem zu übernehmen, das deine Uhr bereits aufgezeichnet hat, statt jede Zahl von Hand einzutippen.</string> - <string name="health_connect_rationale_headline">Amethyst nutzt Health Connect, um „Meine Fitness“ aufzubauen — deine persönliche Trainingsübersicht, auf deinem Telefon.</string> - <string name="health_connect_rationale_intro">„Meine Fitness“ (Menü → Du → Meine Fitness) zeigt dir, wie du trainiert hast: wie viel du diese Woche im Vergleich zur letzten gemacht hast, wie sich deine Zeit auf Laufen, Radfahren, Gehen, Schwimmen und Fitnessstudio verteilt, deine Bestleistungen, an wie vielen Tagen du trainiert hast und deine aktuelle Serie. Amethyst erstellt dieses Bild aus den Trainings, die deine Uhr oder Fitness-App bereits in Health Connect gespeichert hat, zusammen mit allen Trainings, die du in Amethyst selbst erfasst hast. Es wird auf deinem Telefon berechnet und nur dir angezeigt.</string> - <string name="health_connect_rationale_exercise">Training · welche Aktivität du gemacht hast, wann sie begann und wie lange sie dauerte. Das ist das Training selbst: Daraus ergeben sich deine Anzahl an Trainings, deine Trainingszeit, die Aufschlüsselung nach Aktivität, deine aktiven Tage und deine Serie.</string> - <string name="health_connect_rationale_distance">Distanz · wie weit du gekommen bist, angezeigt als deine Distanz dieser Woche im Vergleich zur letzten, dein Wochendurchschnitt, deine Distanz pro Aktivität und deine längste Distanz.</string> - <string name="health_connect_rationale_calories">Aktive und Gesamtkalorien · die Energie, die ein Training verbrannt hat, angezeigt als deine Energie dieser Woche und im Vergleich zur letzten Woche. Aktive Kalorien werden verwendet, wenn deine Quelle sie erfasst; Gesamtkalorien dienen als Ersatz für Quellen, die nur den Gesamtenergieverbrauch erfassen.</string> - <string name="health_connect_rationale_heart_rate">Herzfrequenz · Durchschnitt und Maximum jedes Trainings, zusammengefasst zu deiner durchschnittlichen und höchsten Herzfrequenz im Zeitraum. So unterscheidest du eine lockere Woche von einer harten bei gleicher Distanz.</string> - <string name="health_connect_rationale_steps">Schritte · die Schrittzahl eines Laufs, Spaziergangs oder einer Wanderung, angezeigt als dein wöchentlicher Schrittdurchschnitt und deine höchste Schrittzahl.</string> - <string name="health_connect_rationale_elevation">Höhenmeter · wie viel du aufgestiegen bist, angezeigt als dein wöchentlicher Durchschnitt an Höhenmetern und dein größter Anstieg. Daran erkennst du eine flache Woche von einer hügeligen.</string> - <string name="health_connect_rationale_limit_window">Liest nur Trainings der letzten 4 Wochen, und nur solange „Meine Fitness“ oder der Trainings-Editor geöffnet ist. Liest nie im Hintergrund.</string> - <string name="health_connect_rationale_limit_publish">Sendet deine Gesundheitsdaten nirgendwohin. „Meine Fitness“ wird auf deinem Telefon berechnet und bleibt dort. Wenn du später ein Training posten möchtest, tippst du bei diesem Training auf Teilen, liest den Beitrag, den Amethyst ausgefüllt hat, und veröffentlichst ihn selbst — ein Training nach dem anderen, nie automatisch.</string> - <string name="health_connect_rationale_limit_optional">Die gesamte Funktion ist optional. Widerrufe die Berechtigungen jederzeit in Health Connect — „Meine Fitness“ zeigt dann wieder die Aufforderung — oder schalte die Editor-Vorschläge unter Einstellungen → Editor-Einstellungen aus. Der Rest von Amethyst funktioniert weiter.</string> - <string name="my_fitness_title">Meine Fitness</string> - <string name="my_fitness_window">Letzte 4 Wochen</string> - <string name="my_fitness_loading_metrics">Distanz, Kalorien und Herzfrequenz werden geladen…</string> - <string name="my_fitness_window_note">Die letzten vier vollen Wochen deines Trainings: die Trainings, die du veröffentlicht hast, plus alles, was Health Connect auf diesem Telefon erfasst. Amethyst berechnet die Übersicht auf dem Gerät und veröffentlicht nichts davon.</string> - <string name="my_fitness_this_week">Diese Woche</string> - <string name="my_fitness_vs_last_week">ggü. letzter Woche</string> - <string name="my_fitness_weekly_average">Wochendurchschnitt</string> - <string name="my_fitness_workouts">Trainings</string> - <string name="my_fitness_active_days">Aktive Tage</string> - <string name="my_fitness_streak">Tage in Folge</string> - <string name="my_fitness_time">Zeit</string> - <string name="my_fitness_duration_hours_minutes">%1$d Std. %2$d Min.</string> - <string name="my_fitness_duration_minutes">%1$d Min.</string> - <string name="my_fitness_duration_seconds">%1$d Sek.</string> - <string name="my_fitness_distance">Distanz</string> - <string name="my_fitness_calories">Kalorien</string> - <string name="my_fitness_steps">Schritte</string> - <string name="my_fitness_elevation">Höhenmeter</string> - <string name="my_fitness_avg_heart_rate">Ø HF</string> - <string name="my_fitness_max_heart_rate">Max. HF</string> - <string name="my_fitness_by_activity">Nach Aktivität</string> - <string name="my_fitness_bests">Bestleistungen</string> - <string name="my_fitness_best_longest_distance">Längste Distanz</string> - <string name="my_fitness_best_longest_duration">Längstes Training</string> - <string name="my_fitness_best_biggest_climb">Größter Anstieg</string> - <string name="my_fitness_best_most_steps">Meiste Schritte</string> - <string name="my_fitness_best_highest_heart_rate">Höchste Herzfrequenz</string> - <string name="my_fitness_recent">Letzte Trainings</string> - <string name="my_fitness_empty">In den letzten 4 Wochen wurde nichts erfasst. Trainings, die du postest, und alle, die deine Uhr aufzeichnet, erscheinen hier.</string> - <string name="my_fitness_connect_title">Verfolge dein Training</string> - <string name="my_fitness_connect_message">Verbinde Health Connect, um deine Wochensummen, den Trend im Wochenvergleich, deine Bestleistungen und deine Trainingsserie zu sehen. Alles bleibt auf deinem Telefon.</string> - <string name="my_fitness_connect_banner">Verbinde Health Connect, um die Trainings deiner Uhr mit Herzfrequenz, Schritten und Höhenmetern hinzuzufügen.</string> - <string name="my_fitness_connect_button">Verbinden</string> - <string name="my_fitness_share">Dieses Training teilen</string> - <string name="my_fitness_unit_bpm">S/min</string> - <string name="my_fitness_unit_kcal">kcal</string> - <string name="my_fitness_unit_km">km</string> - <string name="my_fitness_unit_mi">mi</string> - <string name="my_fitness_unit_m">m</string> - <string name="my_fitness_unit_ft">ft</string> - <string name="looking_for_event_title">Wird geladen</string> - <string name="looking_for_private_event">Warte, bis deine privaten Nachrichten geladen sind…</string> - <string name="call_duration_minutes_seconds">%1$02d:%2$02d</string> - <string name="call_duration_hours_minutes_seconds">%1$d:%2$02d:%3$02d</string> - <string name="marmot_preview_group_updated">Gruppe aktualisiert</string> - <string name="marmot_preview_media">Anhang</string> - <string name="marmot_preview_no_text">Nachricht</string> - <string name="marmot_preview_with_sender">%1$s: %2$s</string> - <string name="marmot_empty_create_action">Gruppe erstellen</string> - <string name="marmot_invite_device_banner">Marmot-Einladungen gehen an ein anderes Gerät und können hier nicht geöffnet werden.</string> - <string name="marmot_invite_device_banner_dismiss">Schließen</string> - <string name="marmot_invite_device_publish">Von diesem Gerät veröffentlichen</string> - <string name="wallet_open_zapped_note">Gezappten Beitrag öffnen</string> - <string name="video_player_settings_action_captions">Untertitel</string> - <string name="video_player_settings_action_captions_description">Untertitel ein- oder ausschalten (ausgeblendet, wenn das Video keine hat)</string> - <string name="app_notification_reply_failed">Nicht gesendet</string> - <string name="app_notification_reply_retry">Erneut versuchen</string> - <string name="app_notification_reply_sending">Wird gesendet…</string> - <string name="app_notification_reply_unconfirmed">Wird noch gesendet…</string> - <string name="app_notification_shortcut_account_removed">Dieses Konto wurde aus Amethyst entfernt.</string> - <string name="captions_off">Aus</string> - <string name="captions_turn_off">Untertitel ausschalten</string> - <string name="captions_turn_on">Untertitel einschalten</string> - <string name="captions_unnamed_track">Untertitel</string> - <string name="kind_app_curation_set">App-Liste</string> - <string name="kind_article_curation_set">Artikelliste</string> - <string name="kind_bookmark_set">Lesezeichen-Set</string> - <string name="kind_favorite_algo_feeds">Lieblings-Feeds</string> - <string name="kind_git_authors">Git-Autoren</string> - <string name="kind_git_repositories">Git-Repositories</string> - <string name="kind_good_wiki_authors">Vertrauenswürdige Wiki-Autoren</string> - <string name="kind_good_wiki_relays">Vertrauenswürdige Wiki-Relays</string> - <string name="kind_interest_set">Interessen</string> - <string name="kind_media_follows">Medien-Follows</string> - <string name="kind_media_starter_pack">Medien-Starterpaket</string> - <string name="kind_mute_set_for_kind">Stummgeschaltet für Typ %1$s</string> - <string name="kind_picture_curation_set">Bilderliste</string> - <string name="kind_simple_groups">Gruppen</string> - <string name="kind_video_collaboration">Video-Zusammenarbeit</string> - <string name="kind_video_list">Videoliste</string> - <string name="kind_video_subtitles">Video-Untertitel</string> - <string name="login_bunker_not_supported">Amethyst für Android kann sich nicht mit einer bunker://-Adresse anmelden. Es gibt solche Adressen heraus, damit andere Apps es um Signaturen bitten können, aber anmelden kann es sich damit nicht. Verwende eine externe Signierer-App oder Amethyst Desktop.</string> - <string name="login_nostrconnect_not_supported">Das ist eine App, die sich mit deinem Signierer verbinden möchte, keine Anmeldemöglichkeit. Melde dich zuerst an und öffne sie dann über den NIP-46-Signierer-Bildschirm.</string> - <string name="my_fitness">Meine Fitness</string> - <string name="nip51_list_has_private_members">Diese Liste hat private Mitglieder, die nur ihr Autor sehen kann</string> - <string name="route_edit_geocache">Cache bearbeiten</string> - <string name="route_edit_geocache_hunt">Tour bearbeiten</string> - <string name="route_geocache_detail">Cache</string> - <string name="route_geocache_hunt_detail">Tour</string> - <string name="route_geocache_hunts">Cache-Touren</string> - <string name="route_geocaches">Geocaches</string> - <string name="route_log_geocache_find">Fund loggen</string> - <string name="route_new_geocache">Cache verstecken</string> - <string name="route_new_geocache_hunt">Neue Tour</string> - <string name="video_collaboration_accepted_on">Hat eine Mitwirkungsnennung angenommen</string> - <string name="video_collaboration_declined_on">Hat eine Mitwirkungsnennung abgelehnt</string> - <string name="video_credit_accepted">Nennung angenommen</string> - <string name="video_credit_audio_from">Audio von</string> - <string name="video_credit_featuring">Mit</string> - <string name="video_credit_inspired_by">Inspiriert von</string> - <string name="video_credit_references">Verweise</string> - <string name="video_curation_set_private_items">Private Liste</string> - <plurals name="video_curation_set_video_count"> - <item quantity="one">%1$d Video</item> - <item quantity="other">%1$d Videos</item> - </plurals> - <string name="marmot_invite_device_success">Dieses Gerät empfängt jetzt deine Marmot-Einladungen.</string> - <string name="marmot_invite_device_failure">KeyPackage konnte nicht veröffentlicht werden: %1$s</string> - <string name="marmot_invite_device_rejected">Kein Relay hat das KeyPackage angenommen. Prüfe deine Relay-Einstellungen und versuche es erneut.</string> - <string name="chat_delivery_sending">Wird gesendet…</string> - <string name="chat_delivery_failed">Konnte nicht gesendet werden. Tippe, um es erneut zu versuchen.</string> - <plurals name="marmot_unread_messages"> - <item quantity="one">%1$d neue Nachricht</item> - <item quantity="other">%1$d neue Nachrichten</item> - </plurals> - <string name="chat_type_cordn_title">Cordn-Gruppen</string> - <string name="chat_type_cordn_desc">Ende-zu-Ende-verschlüsselte Gruppen, die von einem Koordinator deiner Wahl statt von Relays geordnet werden.</string> - <string name="new_conversation_cordn_title">Cordn-Gruppe</string> - <string name="new_conversation_cordn_tagline">Verschlüsselter Gruppenchat, geordnet von einem Koordinator deiner Wahl.</string> - <string name="new_conversation_cordn_chip">Koordiniert</string> - <string name="new_conversation_cordn_best">Ideal für Teams, die eine verlässliche Reihenfolge der Unterhaltung wollen.</string> - <string name="new_conversation_cordn_cta">Cordn-Gruppe erstellen</string> - <string name="new_conversation_cordn_pro_1">Ende-zu-Ende-verschlüsselt mit MLS — der Koordinator kann nie eine Nachricht lesen.</string> - <string name="new_conversation_cordn_pro_2">Eine gemeinsame Reihenfolge für alle, sodass der Verlauf nicht durch eine falsch gehende Uhr durcheinandergeraten kann.</string> - <string name="new_conversation_cordn_con_1">Der Koordinator erfährt, wer in der Gruppe ist und wann ihr schreibt, auch wenn er nicht lesen kann, was ihr sagt.</string> <string name="sno_object_title">3D-Objekt</string> <string name="sno_object_details">%1$d Eckpunkte · %2$d Flächen</string> <string name="sno_object_unreadable">Dieses 3D-Objekt konnte nicht gelesen werden (Regel %1$s)</string> @@ -5664,6 +5399,7 @@ <item quantity="one">%1$d Byte an Daten, die dieser Client nicht lesen kann</item> <item quantity="other">%1$d Bytes an Daten, die dieser Client nicht lesen kann</item> </plurals> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> <string name="browser_pill_close">Schließen</string> <string name="browser_pill_tile_copy">Link kopieren</string> <string name="browser_pill_tile_find">Suchen</string> @@ -6094,4 +5830,268 @@ <string name="backup_type_trust_providers_explainer">Die Dienste, denen du vertraust, Personen für dich zu bewerten und zu verifizieren.</string> <string name="backup_type_trusted_relays">Liste vertrauenswürdiger Relays</string> <string name="backup_type_trusted_relays_explainer">Relays, denen du vertraust, sodass ohne Nachfrage eine Verbindung aufgebaut wird.</string> + <string name="cordn_action_delete">Löschen</string> + <string name="cordn_action_edit">Bearbeiten</string> + <string name="cordn_action_editing">Deine Nachricht wird bearbeitet</string> + <string name="cordn_action_go_to_message">Zur Nachricht</string> + <string name="cordn_action_pin">Anheften</string> + <string name="cordn_action_reply">Antworten</string> + <string name="cordn_action_unpin">Loslösen</string> + <string name="cordn_admin_action_failed">Diese Änderung konnte nicht vorgenommen werden.</string> + <string name="cordn_admin_coordinator_silent">Der Koordinator hat nicht geantwortet, daher wurde nichts geändert. Versuche es gleich noch einmal.</string> + <string name="cordn_admin_no_key_package">%1$s hat kein KeyPackage bei diesem Koordinator veröffentlicht und kann daher noch nicht hinzugefügt werden. Bitte die Person, cordn zuerst auf diesem Koordinator zu öffnen.</string> + <string name="cordn_admin_no_self_remove">Du kannst dich nicht selbst aus einer cordn-Gruppe entfernen.</string> + <string name="cordn_admin_no_welcome">%1$s konnte kein Zugang zur Gruppe gegeben werden, daher wurde das Hinzufügen abgebrochen.</string> + <string name="cordn_admin_not_a_member">%1$s ist nicht in dieser Gruppe.</string> + <string name="cordn_admin_someone">Diese Person</string> + <string name="cordn_admin_wrong_key_package">Dieser Koordinator hat ein KeyPackage geliefert, das jemand anderem gehört, daher wurde %1$s nicht hinzugefügt. Es wurde nichts geändert.</string> + <string name="cordn_backup_contents_body">Deine Gruppen-Verschlüsselungsschlüssel, deine KeyPackages und jede Nachricht in diesen Gruppen. Wer die Datei öffnet, kann die Unterhaltungen lesen. Sobald sie Amethyst verlässt, schützt sie nur noch die Passphrase.</string> + <string name="cordn_backup_contents_title">Was in der Datei steckt</string> + <string name="cordn_backup_explainer">Dein nsec kann cordn-Gruppen nicht zurückholen. Eine cordn-Gruppe existiert als Verschlüsselungszustand auf diesem Gerät und als geordneter Datenstrom auf einem Koordinator, der ihn nicht lesen kann — verlierst du das Gerät ohne Backup, ist die Gruppe weg, samt allem, was darin schon gesagt wurde.</string> + <string name="cordn_backup_export">Exportieren</string> + <string name="cordn_backup_exported">Backup gespeichert.</string> + <string name="cordn_backup_failed">Das hat nicht funktioniert. Prüfe die Passphrase und die Datei.</string> + <string name="cordn_backup_passphrase">Passphrase</string> + <string name="cordn_backup_restore">Wiederherstellen</string> + <string name="cordn_backup_restore_body">Beim Wiederherstellen werden die cordn-Gruppen auf diesem Gerät durch die aus der Datei ersetzt. Das ist für ein Smartphone gedacht, das ein anderes abgelöst hat — nicht, um dieselben Gruppen gleichzeitig auf zwei Geräten zu nutzen.</string> + <string name="cordn_backup_restore_confirm_body">Jede cordn-Gruppe, die jetzt auf diesem Gerät ist, wird entfernt und durch die aus der Datei ersetzt.\n\nStelle nicht auf einem zweiten Smartphone wieder her, während das erste noch genutzt wird. Beide hätten dieselben Gruppenschlüssel, und sobald beide eine Nachricht gesendet haben, geht die Gruppe für alle kaputt — unbemerkt und ohne Weg zurück.</string> + <string name="cordn_backup_restore_confirm_title">Die cordn-Gruppen dieses Geräts ersetzen?</string> + <string name="cordn_backup_restore_title">Wiederherstellen</string> + <string name="cordn_backup_restored">Wiederhergestellt.</string> + <string name="cordn_chat_empty_description">Alles, was hier gesendet wird, ist für diese Gruppe verschlüsselt. Der Koordinator leitet es weiter, ohne es lesen zu können.</string> + <string name="cordn_chat_empty_title">Noch keine Nachrichten</string> + <string name="cordn_chat_unread_divider">Neue Nachrichten</string> + <string name="cordn_chat_yesterday">Gestern</string> + <string name="cordn_composer_hint">Nachricht</string> + <string name="cordn_coordinators_add_action">Hinzufügen</string> + <string name="cordn_coordinators_add_failed">Dieser Koordinator konnte nicht geöffnet werden.</string> + <string name="cordn_coordinators_copy_key">npub des Koordinators kopieren</string> + <string name="cordn_coordinators_discover_action">Nach Koordinatoren suchen</string> + <string name="cordn_coordinators_discover_add">Hinzufügen</string> + <string name="cordn_coordinators_discover_explainer">Liest Ankündigungen, die deine Relays ohnehin führen. Kein Koordinator wird kontaktiert, daher erfährt keiner, dass du gesucht hast.</string> + <string name="cordn_coordinators_discover_failed">Ankündigungen konnten nicht gelesen werden</string> + <string name="cordn_coordinators_discover_none">Auf deinen Relays kündigt sich niemand an.</string> + <string name="cordn_coordinators_discover_seen">Vor %1$s angekündigt</string> + <string name="cordn_coordinators_discover_seen_on">Zuletzt angekündigt %1$s</string> + <plurals name="cordn_coordinators_discover_unheard"> + <item quantity="one">Nichts gefunden, und %1$d deiner Relays hat nicht geantwortet.</item> + <item quantity="other">Nichts gefunden, und %1$d deiner Relays haben nicht geantwortet.</item> + </plurals> + <string name="cordn_coordinators_explainer">Ein Koordinator legt die Reihenfolge der Nachrichten einer Gruppe fest und ist der Einzige, der das kann. Geht er verloren, ist die Reihenfolge verloren, und ein zweiter spiegelt den ersten nicht — daher sind das benannte Dienste, die du auswählst, keine austauschbaren Relays.</string> + <plurals name="cordn_coordinators_health_down"> + <item quantity="one">Antwortet nicht — %1$d Anfrage in Folge ist fehlgeschlagen. Gruppen darauf werden nicht synchronisiert.</item> + <item quantity="other">Antwortet nicht — %1$d Anfragen in Folge sind fehlgeschlagen. Gruppen darauf werden nicht synchronisiert.</item> + </plurals> + <string name="cordn_coordinators_health_ok">Antwortet.</string> + <string name="cordn_coordinators_health_retrying">Eine Anfrage ist fehlgeschlagen; neuer Versuch läuft.</string> + <string name="cordn_coordinators_health_unknown">Noch nichts angefragt.</string> + <string name="cordn_coordinators_hide_older">Die ausblenden, die sich nicht mehr ankündigen</string> + <string name="cordn_coordinators_identify">Fragen, wer er ist</string> + <string name="cordn_coordinators_key">Koordinator-Schlüssel</string> + <string name="cordn_coordinators_label">Ein Name dafür (deiner, optional)</string> + <string name="cordn_coordinators_label_note">Dieser Name gehört dir und bleibt auf diesem Gerät. Ein Koordinator kann keinen Namen beweisen, daher wird er nie nach einem gefragt.</string> + <string name="cordn_coordinators_more">Weitere Aktionen</string> + <string name="cordn_coordinators_none">Noch keine Koordinatoren.</string> + <string name="cordn_coordinators_purge">Löschen</string> + <string name="cordn_coordinators_purge_body">Beim Entfernen bleiben die Gruppen auf diesem Gerät, sodass sie zurückkommen, wenn du den Koordinator wieder hinzufügst. Beim Löschen werden sie vernichtet: Die Gruppenschlüssel, die Lesepositionen und deine KeyPackages verschwinden, und diese Gruppen können nur über eine neue Einladung wieder betreten werden.\n\nDer Koordinator wird nicht benachrichtigt und behält alles, was er bereits hatte.</string> + <string name="cordn_coordinators_purge_title">Diesen Koordinator löschen?</string> + <string name="cordn_coordinators_relays">%1$s</string> + <string name="cordn_coordinators_relays_label">Antwortet auf</string> + <string name="cordn_coordinators_relays_more">%1$s +%2$d weitere</string> + <string name="cordn_coordinators_remove">Entfernen</string> + <string name="cordn_coordinators_rename">Umbenennen</string> + <string name="cordn_coordinators_server">Gibt an: %1$s</string> + <string name="cordn_coordinators_server_claim">Name und Version eines Koordinators sind selbst gewählte Angaben. Nur sein öffentlicher Schlüssel identifiziert ihn.</string> + <string name="cordn_coordinators_server_silent">Hat geantwortet, aber nichts angegeben.</string> + <string name="cordn_coordinators_show_all">Alle %1$d anzeigen</string> + <string name="cordn_coordinators_show_fewer">Weniger anzeigen</string> + <plurals name="cordn_coordinators_show_older"> + <item quantity="one">%1$d anzeigen, der sich nicht mehr ankündigt</item> + <item quantity="other">%1$d anzeigen, die sich nicht mehr ankündigen</item> + </plurals> + <string name="cordn_coordinators_stale_note">Diese haben sich seit über einem Monat nicht angekündigt. Eine Gruppe auf einem verschwundenen Koordinator zu erstellen, schlägt fehl.</string> + <string name="cordn_create_action">Gruppe erstellen</string> + <plurals name="cordn_create_action_invite"> + <item quantity="one">Erstellen und %1$d Person einladen</item> + <item quantity="other">Erstellen und %1$d Personen einladen</item> + </plurals> + <string name="cordn_create_add_member">Jemanden hinzufügen</string> + <string name="cordn_create_admin">Admin</string> + <plurals name="cordn_create_admin_count"> + <item quantity="one">%1$d Person kann hinzufügen und entfernen</item> + <item quantity="other">%1$d Personen können hinzufügen und entfernen</item> + </plurals> + <string name="cordn_create_admin_only_me">Nur ich verwalte diese Gruppe</string> + <string name="cordn_create_admin_only_me_on">Nur du kannst einladen, Mitglieder entfernen oder die Gruppendetails ändern. Du kannst das später an jemand anderen übergeben.</string> + <string name="cordn_create_coordinator">Koordinator</string> + <string name="cordn_create_coordinator_change">Ändern</string> + <string name="cordn_create_coordinator_discover">Koordinatoren finden</string> + <string name="cordn_create_coordinator_hide">Fertig</string> + <string name="cordn_create_coordinator_new">Anderer Koordinator</string> + <string name="cordn_create_coordinator_pubkey">Öffentlicher Schlüssel des Koordinators (hex oder npub)</string> + <string name="cordn_create_coordinator_relays">Relays, auf denen er antwortet, eines pro Zeile</string> + <plurals name="cordn_create_coverage_all"> + <item quantity="one">Erreicht deine %1$d Person</item> + <item quantity="other">Erreicht alle %1$d deiner Personen</item> + </plurals> + <string name="cordn_create_coverage_partial">Erreicht %1$d von %2$d — die übrigen blieben außen vor</string> + <string name="cordn_create_coverage_unknown">Nicht geprüft — bei Auswahl wird nachgefragt</string> + <string name="cordn_create_description">Beschreibung (optional)</string> + <string name="cordn_create_egalitarian">Alle können hinzufügen und entfernen</string> + <string name="cordn_create_egalitarian_note">Alle in der Gruppe können einladen, Mitglieder entfernen und die Details ändern. Jedes Mitglied kann später Admins ernennen, womit das endet.</string> + <string name="cordn_create_explainer">Eine cordn-Gruppe wird von einem Koordinator deiner Wahl geordnet. Die Gruppe wird auf diesem Gerät erstellt — der Koordinator erfährt erst von ihr, wenn du jemanden einlädst oder eine Nachricht sendest.</string> + <string name="cordn_create_failed">Die Gruppe konnte nicht erstellt werden.</string> + <string name="cordn_create_name">Gruppenname</string> + <string name="cordn_create_no_coordinator">Kein Koordinator ausgewählt</string> + <string name="cordn_create_outcome_explainer">Jede Einladung ist eine Welcome-Nachricht, die beim Koordinator hinterlegt wird, daher können sie einzeln fehlschlagen. Niemand wurde benachrichtigt: Die Eingeladenen sehen sie, wenn sie ihre App das nächste Mal öffnen.</string> + <string name="cordn_create_outcome_failed">Der Koordinator hat die Einladung abgelehnt</string> + <string name="cordn_create_outcome_link">Link senden</string> + <string name="cordn_create_outcome_no_welcome">Zur Gruppe hinzugefügt, aber die Einladung hat die Person nicht erreicht — versuche es auf dem Gruppenbildschirm erneut</string> + <string name="cordn_create_outcome_open">Chat öffnen</string> + <string name="cordn_create_outcome_skipped">Kein Schlüssel hier, daher konnte keine Welcome-Nachricht hinterlegt werden</string> + <string name="cordn_create_outcome_title">Die Gruppe existiert</string> + <string name="cordn_create_outcome_waiting">Warte auf die Person</string> + <string name="cordn_create_people_done">Fertig</string> + <string name="cordn_create_people_empty">Noch niemand hinzugefügt. Eine Gruppe kann leer erstellt und Einladungen später gesendet werden, aber wenn du jetzt Personen nennst, zeigt sich, welcher Koordinator sie erreichen kann.</string> + <string name="cordn_create_people_none">Noch niemand — tippen, um Personen hinzuzufügen</string> + <string name="cordn_create_people_note">Wenn du jetzt Personen nennst, erfahren wir, welcher Koordinator sie erreichen kann. Jede erhält eine Welcome-Nachricht auf diesem Koordinator, die sie abholt, wenn sie ihre App das nächste Mal öffnet.</string> + <plurals name="cordn_create_reach_count"> + <item quantity="one">Erreichbar über %1$d Koordinator</item> + <item quantity="other">Erreichbar über %1$d Koordinatoren</item> + </plurals> + <string name="cordn_create_reach_none">Kein Schlüssel auf einem deiner Koordinatoren</string> + <string name="cordn_create_reach_unknown">Noch nicht geprüft</string> + <string name="cordn_create_step_admins">Wer hinzufügen und entfernen kann</string> + <string name="cordn_create_step_name">Name</string> + <string name="cordn_create_step_people">Wer dabei ist</string> + <string name="cordn_create_step_where">Wo sie liegt</string> + <string name="cordn_create_title">Neue Cordn-Gruppe</string> + <plurals name="cordn_create_unreachable_count"> + <item quantity="one">%1$d Person ist über keinen deiner Koordinatoren erreichbar</item> + <item quantity="other">%1$d Personen sind über keinen deiner Koordinatoren erreichbar</item> + </plurals> + <string name="cordn_create_you">Du, immer Admin</string> + <string name="cordn_delete_confirm_body">Alle in der Gruppe sehen diese Nachricht als zurückgezogen, und Amethyst zeigt ihren Text nicht mehr an. Der Koordinator behält die verschlüsselte Kopie, als die sie gesendet wurde, und wer sie schon gelesen hat, hat sie gelesen.</string> + <string name="cordn_delete_confirm_title">Nachricht zurückziehen</string> + <string name="cordn_delivery_accepted">Vom Koordinator angenommen</string> + <string name="cordn_exposure_close">Verstanden</string> + <string name="cordn_exposure_open">Was dieser Koordinator sehen kann</string> + <string name="cordn_group_info">Gruppeninfo</string> + <string name="cordn_group_unavailable">Diese Gruppe ist nicht verfügbar. Ihr Koordinator wurde möglicherweise entfernt.</string> + <string name="cordn_group_unavailable_detail">Füge in den Einstellungen einen Koordinator hinzu, um cordn-Gruppen zu nutzen.</string> + <plurals name="cordn_groups_count"> + <item quantity="one">%1$d Gruppe</item> + <item quantity="other">%1$d Gruppen</item> + </plurals> + <string name="cordn_groups_down_note">Aus diesen Gruppen kommt nichts an. Amethyst versucht es weiter.</string> + <string name="cordn_groups_manage">Koordinatoren</string> + <string name="cordn_groups_none">Noch keine cordn-Gruppen</string> + <string name="cordn_groups_none_detail">Eine cordn-Gruppe liegt auf einem Koordinator, der ihre Mitgliedschaft verwaltet und Nachrichten weitergibt, ohne je zu sehen, was jemand schreibt.</string> + <string name="cordn_groups_start">Eine starten</string> + <string name="cordn_info_add_member">Jemanden hinzufügen</string> + <string name="cordn_info_add_member_none">Niemand mit diesem Namen gefunden.</string> + <string name="cordn_info_add_member_note">Personen können nur hinzugefügt werden, wenn sie ein KeyPackage bei diesem Koordinator veröffentlicht haben.</string> + <string name="cordn_info_add_member_placeholder">Name, npub oder name@domain</string> + <string name="cordn_info_admin_badge">Admin</string> + <plurals name="cordn_info_admins_kept"> + <item quantity="one">Der %1$d Admin der Gruppe bleibt unverändert.</item> + <item quantity="other">Die %1$d Admins der Gruppe bleiben unverändert.</item> + </plurals> + <string name="cordn_info_coordinator">Koordinator</string> + <string name="cordn_info_coordinator_key">Koordinator-Schlüssel</string> + <string name="cordn_info_coordinator_nprofile">Koordinator-Link</string> + <string name="cordn_info_copy_nprofile">nprofile des Koordinators kopieren</string> + <string name="cordn_info_edit_details">Details bearbeiten</string> + <string name="cordn_info_egalitarian">Diese Gruppe hat keine Admins: Alle können dauerhaft hinzufügen, entfernen und umbenennen.</string> + <string name="cordn_info_epoch">Epoche</string> + <string name="cordn_info_gid">Gruppen-ID</string> + <string name="cordn_info_has_key_package">Kann über diesen Koordinator hinzugefügt werden</string> + <string name="cordn_info_members">Mitglieder</string> + <string name="cordn_info_remove_confirm_body">%1$s erhält von hier keine Nachrichten mehr. Bereits Gelesenes bleibt erhalten, und das lässt sich nicht rückgängig machen — die Person müsste erneut eingeladen werden.</string> + <string name="cordn_info_remove_confirm_title">Aus der Gruppe entfernen?</string> + <string name="cordn_info_remove_member">Entfernen</string> + <string name="cordn_info_save">Speichern</string> + <string name="cordn_info_technical">Technische Details</string> + <string name="cordn_invitations_accept">Beitreten</string> + <string name="cordn_invitations_decline">Ablehnen</string> + <string name="cordn_invitations_decline_warning">Ablehnen ist endgültig. Um wieder hineinzukommen, musst du erneut eingeladen werden.</string> + <string name="cordn_invitations_explainer">Wird nur geprüft, wenn du diesen Bildschirm öffnest, und sonst nie — einen Koordinator zu fragen, ob dich jemand eingeladen hat, verrät ihm, dass du da bist, daher tut Amethyst das nicht im Hintergrund.</string> + <string name="cordn_invitations_load_failed">Einladungen konnten nicht gelesen werden.</string> + <plurals name="cordn_invitations_members"> + <item quantity="one">%1$d Person ist bereits in dieser Gruppe</item> + <item quantity="other">%1$d Personen sind bereits in dieser Gruppe</item> + </plurals> + <string name="cordn_invitations_members_more">+%1$d weitere</string> + <string name="cordn_invitations_no_coordinators">Du hast keinen Koordinator geöffnet. cordn-Einladungen kommen über einen Koordinator an, daher gibt es noch nirgends etwas nachzusehen.</string> + <string name="cordn_invitations_none">Keine wartenden Einladungen.</string> + <string name="cordn_invitations_refresh">Erneut prüfen</string> + <string name="cordn_invitations_skipped">Eine Einladung wurde für ein anderes Gerät hinterlegt</string> + <string name="cordn_invitations_title">Cordn-Einladungen</string> + <string name="cordn_invitations_unreachable">%1$s hat nicht geantwortet</string> + <string name="cordn_invitations_via">Über %1$s</string> + <string name="cordn_keypackages_disclosure">Beim Veröffentlichen wird ein Eintrag mit deinem eigenen Kontoschlüssel signiert. Dieser Koordinator weiß dann, dass dieses Konto existiert und eingeladen werden kann — dauerhaft, und unabhängig davon, ob dich jemand einlädt.</string> + <string name="cordn_keypackages_explainer">Andere können dich nur zu einer cordn-Gruppe hinzufügen, indem sie ein KeyPackage verwenden, das du bei diesem Koordinator veröffentlicht hast. cordn hat keinen anderen Ort dafür, also kann dich ohne veröffentlichtes KeyPackage niemand einladen — und nichts sagt ihnen, warum.</string> + <string name="cordn_keypackages_failed">Die KeyPackages konnten nicht gelesen werden.</string> + <string name="cordn_keypackages_last_resort_no">Kein Last-Resort-Paket. Sobald die Einmal-Pakete aufgebraucht sind, schlagen Einladungen fehl.</string> + <string name="cordn_keypackages_last_resort_yes">Ein Last-Resort-Paket ist veröffentlicht, daher sind Einladungen auch dann noch möglich, wenn die Einmal-Pakete aufgebraucht sind.</string> + <plurals name="cordn_keypackages_orphans"> + <item quantity="one">%1$d Paket, das dieses Gerät nicht öffnen kann</item> + <item quantity="other">%1$d Pakete, die dieses Gerät nicht öffnen kann</item> + </plurals> + <string name="cordn_keypackages_orphans_body">Der Koordinator gibt diese an jeden weiter, der dich einlädt, aber die private Hälfte ist nicht auf diesem Gerät — sie gehört zu einer anderen Installation. Gibt es diese Installation nicht mehr, erzeugt eine Einladung mit einem dieser Pakete eine Welcome-Nachricht, die niemand je öffnen kann.</string> + <string name="cordn_keypackages_publish">Eines veröffentlichen</string> + <string name="cordn_keypackages_publish_last_resort">Last-Resort veröffentlichen</string> + <plurals name="cordn_keypackages_summary"> + <item quantity="one">%1$d Einmal-Paket verfügbar.</item> + <item quantity="other">%1$d Einmal-Pakete verfügbar.</item> + </plurals> + <string name="cordn_keypackages_topup_note">Sobald du hier veröffentlicht hast, ersetzt Amethyst verbrauchte Pakete unauffällig. Das erste veröffentlicht es nie für dich.</string> + <string name="cordn_keypackages_withdraw_all">Alle zurückziehen</string> + <string name="cordn_keypackages_withdraw_orphans">Diese zurückziehen</string> + <string name="cordn_link_request">Beitritt zu dieser Gruppe anfragen</string> + <string name="cordn_link_request_disclosure">Dies veröffentlicht ein KeyPackage unter deinem eigenen Kontoschlüssel und teilt dem Koordinator mit, dass du in diese Gruppe möchtest — unabhängig davon, ob jemand antwortet.</string> + <string name="cordn_link_request_failed">Beitrittsanfrage fehlgeschlagen.</string> + <string name="cordn_link_request_sent">Angefragt. Ein Mitglied der Gruppe muss dich hinzufügen; die Einladung erscheint unter cordn-Einladungen.</string> + <string name="cordn_link_scan">Scannen</string> + <string name="cordn_media_download_failed">Diese Datei konnte nicht geöffnet werden.</string> + <string name="cordn_media_no_server">Für dieses Konto ist kein Medienserver festgelegt, daher gibt es keinen Ablageort für die Datei. Wähle einen in den Einstellungen.</string> + <string name="cordn_media_unreadable">Diese Datei konnte nicht gelesen werden.</string> + <string name="cordn_media_upload_failed">Diese Datei konnte nicht gesendet werden.</string> + <plurals name="cordn_member_count"> + <item quantity="one">%1$d Mitglied</item> + <item quantity="other">%1$d Mitglieder</item> + </plurals> + <string name="cordn_message_deleted">Nachricht gelöscht</string> + <string name="cordn_message_details_coordinator">Koordinator</string> + <string name="cordn_message_details_cursor">Cursor</string> + <string name="cordn_message_details_sent_at">Gesendet</string> + <string name="cordn_message_details_title">Nachrichtendetails</string> + <string name="cordn_message_edited">bearbeitet</string> + <string name="cordn_pinned_by">Angeheftet von %1$s</string> + <plurals name="cordn_pinned_count"> + <item quantity="one">%1$d angeheftete Nachricht in dieser Gruppe</item> + <item quantity="other">%1$d angeheftete Nachrichten in dieser Gruppe</item> + </plurals> + <string name="cordn_pinned_next">Nächste angeheftete Nachricht</string> + <string name="cordn_pinned_previous">Vorherige angeheftete Nachricht</string> + <string name="cordn_pinned_show_all">Alle angehefteten Nachrichten anzeigen</string> + <string name="cordn_pinned_title">Angeheftete Nachrichten</string> + <string name="cordn_reactions_title">Reaktionen</string> + <string name="cordn_requests_accept">Hinzufügen</string> + <string name="cordn_requests_admin_only">Nur ein Admin kann annehmen. In einer Gruppe ohne Admins sind das alle.</string> + <string name="cordn_requests_check">Nach Anfragen suchen</string> + <string name="cordn_requests_decline">Verwerfen</string> + <string name="cordn_requests_failed">Die Anfragen konnten nicht gelesen werden.</string> + <string name="cordn_requests_none">Niemand wartet auf den Beitritt.</string> + <string name="cordn_requests_title">Beitrittsanfragen</string> + <string name="cordn_send">Senden</string> + <string name="cordn_send_failed">Das wurde nicht gesendet.</string> + <string name="cordn_send_no_session">Der Koordinator dieser Gruppe ist nicht geöffnet, daher kann noch nichts gesendet werden.</string> + <string name="cordn_share_copied">Kopiert</string> + <string name="cordn_share_copy">Link kopieren</string> + <string name="cordn_share_explainer">Jeder mit diesem Link kann den Beitritt anfragen. Ein Admin muss ihn trotzdem noch hereinlassen.</string> + <string name="cordn_share_title">Diese Gruppe teilen</string> + <string name="cordn_voice_play">Sprachnachricht abspielen</string> + <string name="cordn_voice_record">Sprachnachricht aufnehmen</string> + <string name="cordn_voice_stop">Stoppen und senden</string> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml b/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml index d4f890da43..9f928a700e 100644 --- a/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml @@ -1862,7 +1862,7 @@ <string name="media_compression_quality_medium">मध्यम</string> <string name="media_compression_quality_high">उच्च</string> <string name="media_compression_quality_uncompressed">असंकुचित</string> - <string name="metadata_strip_failed_body">यह अभिलेख प्रारूप परितथ्य हटाने का अवलम्बन नहीं करता। निजी जानकारी जैसे स्थान तथा यन्त्र विवरण समाविष्ट हो सकते हैं। क्या तो भी आरोहण करें।</string> + <string name="metadata_strip_failed_body">यह अभिलेख प्रारूप उपतथ्य हटाने का अवलम्बन नहीं करता। निजी जानकारी जैसे स्थान तथा यन्त्र विवरण समाविष्ट हो सकते हैं। क्या तो भी आरोहण करें।</string> <string name="metadata_strip_failed_upload">तो भी आरोहण करें</string> <string name="edit_draft">पाण्डुलिपि सम्पादन</string> <string name="login_with_qr_code">क्यूआर॰ चित्र अक्षरराशि के साथ प्रवेशांकन करें</string> @@ -2889,6 +2889,7 @@ <string name="community_rules_violation_wot_gate_failed">आप इस समुदाय के विश्वासजाल आवश्यकताओं को पार नहीं कर सके।</string> <string name="community_rules_violation_stale_rules">समुदाय का नवीनतम नियम अभिलेख पुराना होने के कारण अस्वीकृत।</string> <string name="marmot_groups_title">मार्मोट॰ समूह</string> + <string name="cordn_groups_title">कोर्डन समूह</string> <string name="marmot_create_group">समूह बनाएँ</string> <string name="marmot_create_group_title">मार्मोट॰ समूह बनाएँ</string> <string name="marmot_create_group_footer">एक नया एमएलएस॰ समूह बनेगा। आप सदस्य जोड सकते हैं तत्पश्चात।</string> @@ -3248,7 +3249,7 @@ <string name="automatically_show_url_preview_description">जालपता के पूर्वीक्षण दिखाएँ</string> <string name="autoplay_videos">स्वतःचालन चलचित्र</string> <string name="autoplay_videos_description">स्वतः चालू करें चलचित्रों को जब पटल पर दिखाई दें</string> - <string name="avif_metadata_strip_failed">एविफ॰ से परितथ्य नहीं मिटा सके : %1$s</string> + <string name="avif_metadata_strip_failed">एविफ॰ से उपतथ्य नहीं मिटा सके। %1$s</string> <string name="award_badge">पदक पुरस्कार</string> <string name="back">पीछे चलें</string> <string name="backup_keys">कुंचिकाएँ सुरक्षित रखें</string> @@ -4119,8 +4120,8 @@ <string name="messages_cant_upload_title">आरोहण असफल</string> <string name="messages_new_message_subject">विषय</string> <string name="messages_settings">सन्देश</string> - <string name="metadata_strip_failed_title">परितथ्य हटाने में असफल</string> - <string name="metadata_strip_failed_upload_cancelled">श्रव्यदृश्याभिलेख से निजी परितथ्य हटाने में असफल। आरोहण निरस्त।</string> + <string name="metadata_strip_failed_title">उपतथ्य हटाने में असफल</string> + <string name="metadata_strip_failed_upload_cancelled">श्रव्यदृश्याभिलेख से निजी उपतथ्य हटाने में असफल। आरोहण निरस्त।</string> <string name="migrate_bookmarks_success">स्मर्त्तव्यचिह्न सफलतापूर्वक स्थानान्तरित किए गए</string> <string name="missing_lud16">लैटनिंग स्थापना अनुपलब्ध</string> <string name="more_options">अधिक विकल्प</string> @@ -5056,8 +5057,8 @@ <string name="spamming_users">कचरालेख प्रेषक</string> <string name="split_notifications_setting_description">दो सूचना पृष्ठसूचक दिखाएँ। अनुचरित (लोग जिनका आप अनुचरण करते हैं) तथा सभी। अपठित संकेतक चमकता है केवल आपके द्वारा अनुचरित लोगों के गतिविधियों के लिए।</string> <string name="split_notifications_setting_title">सूचनाएँ विभाजित करें अनुचरित के अनुसार</string> - <string name="strip_metadata_description">निजी परितथ्य हटाने का प्रयास करता है आलम्बित श्रव्यदृश्याभिलेखों से आरोहण पूर्व</string> - <string name="strip_metadata_label">निजी परितथ्य हटाएँ</string> + <string name="strip_metadata_description">निजी उपतथ्य हटाने का प्रयास करता है आलम्बित श्रव्यदृश्याभिलेखों से आरोहण पूर्व</string> + <string name="strip_metadata_label">निजी उपतथ्य हटाएँ</string> <string name="suggest_workouts_setting_description">सम्पन्न व्यायाम पढता है स्वास्थ्य संयोजन से (सामसंग स्वास्थ्य तथा गूगिल फिट तथा फिटबिट तथा गार्मिन इत्यादि) तथा पत्र के रूप में बाँटने का सुझाव देता है। अनुमति अनुरोध केवल संयोजन करने पर।</string> <string name="suggest_workouts_setting_title">व्यायामों का सुझाव बाँटनें के लिए</string> <string name="system">यन्त्रव्यवस्था</string> @@ -5232,9 +5233,36 @@ <!-- cordn: groups delivered by an MCP coordinator rather than by relays. The exposure strings state what the coordinator operator learns; see spec/00.md §8 and quartz/plans/2026-09-17-cordn-interop.md. --> + <string name="cordn_badge_coordinator">समायोजक समूह</string> + <string name="cordn_badge_coordinator_desc">समायोजक द्वारा भेजा गया। पुनःप्रसारकों द्वारा नहीं</string> + <string name="cordn_exposure_title">यह समायोजक क्या देख सकता है</string> + <string name="cordn_exposure_dimension_content">सन्देश विषयवस्तु</string> + <string name="cordn_exposure_dimension_membership">समूह में कौन हैं</string> + <string name="cordn_exposure_dimension_messaging">कौन क्या भेजता है</string> + <string name="cordn_exposure_level_none">पठन असाध्य</string> + <string name="cordn_exposure_level_pseudonymous">एक अस्थायी कुंचिका द्वारा</string> + <string name="cordn_exposure_level_identified">आपकी वास्तविक लेखा से सम्बद्ध</string> + <string name="cordn_exposure_details">विवरण</string> + <string name="cordn_exposure_coordinator">समायोजक %1$s</string> + <string name="cordn_health_unknown">सम्पर्क अब तक किया नहीं</string> + <string name="cordn_health_ok">प्रतिवचनमध्य</string> + <string name="cordn_health_down">कोई प्रतिवचन नहीं</string> + <string name="cordn_link_field">cordn1…</string> + <string name="cordn_link_inspect">निरीक्षण</string> + <string name="cordn_link_paste">चिपकाएँ</string> + <string name="cordn_link_clear">रिक्त</string> + <string name="cordn_link_invalid">वह एक कोर्डन समूह योजक नहीं। %1$s</string> + <string name="cordn_link_group_id">समूह परिचायक</string> + <string name="cordn_link_coordinator">समायोजक</string> + <string name="cordn_link_relays">अभिगम्य इनके द्वारा</string> <!-- cordn entries in the settings catalog and the Messages chat-type list. Here rather than in amethyst/res because both screens read them through Res. --> + <string name="cordn_link_title">कोर्डन समूह योजक</string> + <string name="cordn_link_search_keywords">कोर्डन समायोजक समूह योजक आमन्त्रण एमएलएस॰ चर्चा उपतथ्य गोपनीयता अनावरण</string> + <string name="cordn_coordinators_title">कोर्डन समायोजक</string> + <string name="cordn_coordinators_search_keywords">कोर्डन समायोजक जोडें हटाएँ मिटाएँ स्वास्थ्य सेवासंगणक एमएलएस॰ चर्चा अनुक्रमण</string> + <string name="cordn_keypackages_title">कोर्डन कुंचिका पोटलियाँ</string> <!-- cordn strings on screens main migrated to Res: the shared inbox row and the new-conversation picker. --> <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a diff --git a/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml b/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml index 04d81c7de6..396bea328a 100644 --- a/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-hu-rHU/strings.xml @@ -17,7 +17,7 @@ <string name="login_hide_key">Kulcs elrejtése</string> <!-- New Key Warning --> <string name="new_key_warning_title">FONTOS: Mentse el a kulcsait!</string> - <string name="new_key_warning_message">A titkos kulcs (nsec) olyan, mint egy jelszó, amelyet SOHA nem lehet visszaállítani vagy helyreállítani. Ez az EGYETLEN módja a fiókhoz való hozzáférésnek, és aki birtokolja, az örökre irányítja ezt a fiókot. Ha elveszíti, a fiókja végleg elvész. Soha ne adja tovább és tárolja egy jelszókezelőben.</string> + <string name="new_key_warning_message">A titkos kulcs (nsec) olyan, mint egy jelszó, amelyet SOHA nem lehet visszaállítani vagy helyrehozni. Ez az EGYETLEN módja a fiókhoz való hozzáférésnek, és aki birtokolja, az örökre irányítja ezt a fiókot. Ha elveszíti, a fiókja végleg elvész. Soha ne adja tovább és tárolja egy jelszókezelőben.</string> <string name="new_key_public_label">Nyilvános kulcs (megosztható):</string> <string name="new_key_secret_label">Titkos kulcs (SOHA ne ossza meg!):</string> <string name="new_key_continue_button">Elmentettem a kulcsaimat, folytatás</string> @@ -33,7 +33,7 @@ <string name="new_key_step_intro_nsec">A titkos kulcs (nsec) egy jelszó, amelyet SOHA nem lehet visszaállítani. Ez az egyetlen alkalom, amikor megjelenik — mentse el most, különben örökre elveszíti a hozzáférését ehhez a fiókhoz.</string> <string name="new_key_keys_title">Saját kulcsok</string> <string name="new_key_confirm_title">Még valami</string> - <string name="new_key_confirm_recap">Tárolja a titkos kulcsát egy jelszókezelőben. Ha elveszíti, akkor senki sem tudja helyreállítani Önnek.</string> + <string name="new_key_confirm_recap">Tárolja a titkos kulcsát egy jelszókezelőben. Ha elveszíti, akkor senki sem tudja helyrehozni Önnek.</string> <string name="new_key_readonly_info">Ez csak egy olvasásra jogosult fiók. Nem jött létre titkos kulcs, így nincs miről biztonsági mentést készíteni.</string> <!-- Key Backup (Desktop settings) --> <string name="backup_keys_title">Kulcsok biztonsági mentése</string> @@ -42,7 +42,7 @@ <string name="backup_keys_show_qr">QR-kód megjelenítése</string> <string name="backup_keys_hide_qr">QR-kód elrejtése</string> <string name="backup_keys_secret_label">Titkos kulcs (nsec)</string> - <string name="backup_keys_secret_warning">A titkos kulcs egy jelszó, amelyet SOHA nem lehet visszaállítani vagy helyreállítani. Aki ismeri, az örökre irányíthatja ezt a fiókot. Soha ne adja meg senkinek és tárolja egy biztonságos jelszókezelőben.</string> + <string name="backup_keys_secret_warning">A titkos kulcs egy jelszó, amelyet SOHA nem lehet visszaállítani vagy helyrehozni. Aki ismeri, az örökre irányíthatja ezt a fiókot. Soha ne adja meg senkinek és tárolja egy biztonságos jelszókezelőben.</string> <string name="backup_keys_secret_hidden">A titkos kulcs rejtve van</string> <string name="backup_keys_reveal">Titkos kulcs megjelenítése</string> <string name="backup_keys_hide">Titkos kulcs elrejtése</string> @@ -654,10 +654,10 @@ <string name="mark_all_known_as_read">Az összes ismert megjelölése olvasottként</string> <string name="mark_all_new_as_read">Az összes új üzenet megjelölése olvasottként</string> <string name="mark_all_as_read">Összes megjelölése olvasottként</string> - <string name="account_backup_tips2_md">## Kulcs- és biztonsági mentési tippek \n\nFiókját egy titkos kulcs védi. A kulcs egy hosszú véletlenszerű karakterlánc, amely **nsec1**-vel kezdődik. Bárki, aki az Ön titkos kulcsához hozzáfér, az Ön személyazonosságának használatával bármilyen tartalmat közzétehet. \n\n- **Ne** helyezze el a titkos kulcsát olyan webhelyen vagy szoftverben, amelyben nem bízik. \n- Az Amethyst fejlesztői az Ön titkos kulcsát **soha** nem fogják Öntől elkérni. \n- A fiók-helyreállításának érdekében, a titkos kulcsáról **mindig** készítsen biztonsági másolatot. Javasoljuk egy jelszókezelő használatát.</string> - <string name="account_backup_tips3_md">A nagyobb biztonság érdekében titkosítsa egy jelszóval. A jelszóval védett kulcs **ncryptsec1**-vel kezdődik, és a jelszava nélkül nem használható. \n\nHa elveszíti a jelszavát, nem tudja visszaállítani a kulcsát.</string> + <string name="account_backup_tips2_md">## Kulcs- és biztonsági mentési tippek \n\nFiókját egy titkos kulcs védi. A kulcs egy hosszú véletlenszerű karakterlánc, amely **nsec1**-vel kezdődik. Bárki, aki az Ön titkos kulcsához hozzáfér, az Ön személyazonosságának használatával bármilyen tartalmat közzétehet. \n\n- **Ne** helyezze el a titkos kulcsát olyan webhelyen vagy szoftverben, amelyben nem bízik. \n- Az Amethyst fejlesztői az Ön titkos kulcsát **soha** nem fogják Öntől elkérni. \n- A fiókja helyrehozása érdekében, a titkos kulcsáról **mindig** készítsen biztonsági másolatot. Javasoljuk egy jelszókezelő használatát.</string> + <string name="account_backup_tips3_md">A nagyobb biztonság érdekében titkosítsa egy jelszóval. A jelszóval védett kulcs **ncryptsec1**-vel kezdődik, és a jelszava nélkül nem használható. \n\nHa elveszíti a jelszavát, nem tudja helyrehozni a kulcsát.</string> <string name="backup_keys_nudge_title">Biztonsági mentés készítése a kulcsokról</string> - <string name="backup_keys_nudge_body">A titkos kulcs megadása az egyetlen módja a fiók elérésének. Ha elveszíti, akkor soha nem tudja visszaszerezni. Mentse el biztonságos helyre most.</string> + <string name="backup_keys_nudge_body">A titkos kulcs megadása az egyetlen módja a fiók elérésének. Ha elveszíti, akkor soha nem tudja helyrehozni. Mentse el biztonságos helyre most.</string> <string name="backup_keys_nudge_backup_now">Biztonsági mentés most</string> <string name="backup_keys_nudge_already_saved">Elmentettem a kulcsokat</string> <string name="backup_keys_nudge_dismiss">Eltüntetés</string> @@ -1021,7 +1021,7 @@ <string name="relay_auth_session_row_desc">Bejelentkezve maradás az Amethyst újraindításáig</string> <string name="relay_auth_forget_session">Bejelentkezés elfelejtése</string> <string name="relay_auth_session_forgotten_undo">Ha szükség lesz rá, a(z) %1$s ismét rákérdez.</string> - <string name="relay_auth_session_undo_blocked">Nincs visszaállítva. A „Soha ne jelentkezzen be” be van kapcsolva, ezért az Amethyst nem fog bejelentkezni ide: %1$s.</string> + <string name="relay_auth_session_undo_blocked">Nincs helyreállítva. A „Soha ne jelentkezzen be” be van kapcsolva, ezért az Amethyst nem fog bejelentkezni ide: %1$s.</string> <string name="relay_auth_blocked_section">Letiltva az Ön tiltólistája alapján</string> <string name="relay_auth_blocked_row_desc">Az Amethyst soha nem jelentkezik be a letiltott átjátszókra.</string> <string name="relay_auth_no_blocked">Semmi sincs letiltva. Az Ön által blokkolt átjátszókra soha nem fog bejelentkezni, függetlenül attól, hogy mit állít be itt.</string> @@ -1927,8 +1927,8 @@ <string name="cashu_wizard_set_main">Beállítás fő pénztárcaként</string> <string name="cashu_wizard_recover_funds">Összegek átmentése a fő pénztárcába</string> <string name="cashu_wizard_recoverable">%1$s satoshi menthető át</string> - <string name="cashu_wizard_recovered">Sikeres átvitel: %1$s satoshi</string> - <string name="cashu_wizard_no_funds">Nincsenek átvihető összegek</string> + <string name="cashu_wizard_recovered">Átmentve: %1$s satoshi</string> + <string name="cashu_wizard_no_funds">Nincsenek átmenthető összegek</string> <string name="cashu_wizard_invalid">Nem sikerült beolvasni ezt a pénztárcát</string> <string name="cashu_wizard_set_main_first">Először állítsa be a fő pénztárcáját, majd mentse át az összegeket a régebbiekből.</string> <string name="cashu_wizard_adopting">Saját pénztárca beállítása…</string> @@ -1971,10 +1971,10 @@ <string name="cashu_settings_delete_recommendation">Ajánlás törlése</string> <string name="cashu_settings_delete_confirm_title">Visszavonja az ajánlást?</string> <string name="cashu_settings_add_recommendation">Egy pénzverde ajánlása</string> - <string name="cashu_settings_restore_title">Visszaállítás seed-ből</string> + <string name="cashu_settings_restore_title">Helyrehozás seed-ből</string> <string name="cashu_settings_restore_subtitle">Minden korábbi titok újralevezetése, és minden mint megkérdezése, hogy visszaadja-e a még nyilvántartásban lévő vak aláírásokat. Hasznos eszközvesztés vagy egy megbízhatatlan átjátszó általi token-esemény törlése esetén.</string> <string name="cashu_settings_restore_running">Pénzverdék vizsgálata…</string> - <string name="cashu_settings_restore_nothing_found">Nem található visszaállítható bizonyíték.</string> + <string name="cashu_settings_restore_nothing_found">Nem található helyreállítható bizonyíték.</string> <string name="cashu_settings_stop_nutzaps">Nutzapok fogadásának leállítása</string> <string name="cashu_settings_stop_nutzaps_subtitle">Vonja vissza a kind:10019 eseményét, hogy mások ne küldhessenek Önnek több nutzapot. Az Ön pénztárcája és egyenlege változatlan marad.</string> <string name="cashu_settings_stop_nutzaps_confirm_title">Leállítja a nutzapok fogadását?</string> @@ -1982,18 +1982,18 @@ <string name="cashu_settings_recreate_key">Nutzap-kulcs újraelőállítása</string> <string name="cashu_settings_recreate_key_subtitle">Egy teljesen új kulcs előállítása a nutzapok fogadásához. Ritkán van rá szükség - általában csak akkor, ha a jelenlegi kulcsa kiszivárgott. A régi kulcsára már elküldött, de még be nem váltott nutzapok elvesznek.</string> <string name="cashu_settings_recreate_key_confirm_title">Újraelőállítja a nutzap-kulcsot?</string> - <string name="cashu_settings_recreate_key_confirm_body">Egy új P2PK kulcs kerül előállításra és közzétételre a kind:10019 és kind:17375 eseményeiben, megtartva a jelenlegi pénzverdéit. A küldők elkezdenek nutzapokat zárolni az új kulcshoz. A régi kulcsára már elküldött, de még be nem váltott nutzapok helyreállíthatatlanná válnak. Erre ritkán van szükség.</string> + <string name="cashu_settings_recreate_key_confirm_body">Egy új P2PK kulcs kerül előállításra és közzétételre a kind:10019 és kind:17375 eseményeiben, megtartva a jelenlegi pénzverdéit. A küldők elkezdenek nutzapokat zárolni az új kulcshoz. A régi kulcsára már elküldött, de még be nem váltott nutzapok helyrehozhatatlanná válnak. Erre ritkán van szükség.</string> <string name="cashu_settings_recreate_key_action">Kulcs újraelőállítása</string> <string name="cashu_settings_import_key">Nutzap-kulcs importálása</string> - <string name="cashu_settings_import_key_subtitle">Cserélje le a nutzap-kulcsát egy beillesztett kulcsra - például a pénztárca biztonsági mentésből történő visszaállításához. Ritkán van rá szükség. A régi kulcsához zárolt nutzapek a továbbiakban nem lesznek beválthatók.</string> + <string name="cashu_settings_import_key_subtitle">Cserélje le a nutzap-kulcsát egy beillesztett kulcsra, például a pénztárca biztonsági mentésből történő helyreállításához. Ritkán van rá szükség. A régi kulcsához zárolt nutzapek a továbbiakban nem lesznek beválthatók.</string> <string name="cashu_settings_import_key_confirm_title">Importálja a nutzap-kulcsot?</string> <string name="cashu_settings_import_key_confirm_body">Illessze be a nutzapok fogadásához használandó P2PK privát kulcsot (hex). A kind:10019 és kind:17375 eseményei újra közzé lesznek téve ezzel a kulccsal, megtartva a jelenlegi pénzverdéit. A jelenlegi kulcsához zárolt nutzapok többé nem lesznek beválthatók, kivéve, ha a kulcs megegyezik. Erre ritkán van szükség.</string> <string name="cashu_settings_import_key_field">P2PK privát kulcs (hex)</string> <string name="cashu_settings_import_key_action">Kulcs importálása</string> <string name="cashu_settings_delete_wallet">Pénztárca törlése</string> - <string name="cashu_settings_delete_wallet_subtitle">Távolítsa el a pénztárcát és állítsa le a nutzapokat. A fennmaradó egyenleg helyreállíthatatlanná válhat.</string> + <string name="cashu_settings_delete_wallet_subtitle">Távolítsa el a pénztárcát és állítsa le a nutzapokat. A fennmaradó egyenleg helyrehozhatatlanná válhat.</string> <string name="cashu_settings_delete_wallet_confirm_title">Törli ezt a pénztárcát?</string> - <string name="cashu_settings_delete_wallet_confirm_body">Ez a művelet a kind:17375 pénztárca és a kind:10019 nutzap-információ törlését kéri. Az ecash-igazolások nem törlődnek a pénzverdékből, de a pénztárca kulcsának eltűnésével a megmaradt egyenleg és a be nem váltott nutzapok helyreállíthatatlanná válhatnak. Győződjön meg arról, hogy először elköltötte vagy áthelyezte a pénzét. Ez a művelet nem vonható vissza.</string> + <string name="cashu_settings_delete_wallet_confirm_body">Ez a művelet a kind:17375 pénztárca és a kind:10019 nutzap-információ törlését kéri. Az ecash-igazolások nem törlődnek a pénzverdékből, de a pénztárca kulcsának eltűnésével a megmaradt egyenleg és a be nem váltott nutzapok helyrehozhatatlanná válhatnak. Győződjön meg arról, hogy először elköltötte vagy áthelyezte a pénzét. Ez a művelet nem vonható vissza.</string> <string name="cashu_pay_invoice">Számla kifizetése</string> <string name="cashu_get_quote">Árajánlat kérése</string> <string name="cashu_getting_quote">Árajánlat kérése a pénzverdétől…</string> @@ -2159,7 +2159,7 @@ <string name="bottom_bar_settings_expand">Beállítások megjelenítése</string> <string name="bottom_bar_settings_reorder_hint">Húzza a rendezéshez, koppintson az ✕ gombra az eltávolításhoz</string> <string name="bottom_bar_settings_remove">Eltávolítás</string> - <string name="bottom_bar_settings_restore_default">Alapértelmezés visszaállítása</string> + <string name="bottom_bar_settings_restore_default">Alapértelmezés helyreállítása</string> <string name="drawer_settings_title">Saját oldalsó menü</string> <string name="drawer_settings_description">Nyisson meg egy szakaszt, és kapcsolja ki azokat a sorokat, amelyeket soha nem használ. A beállítások mindig láthatók maradnak, így bármikor visszatérhet ide. A szakaszok sorrendje rögzített.</string> <string name="drawer_settings_sections">Szakaszok</string> @@ -2244,7 +2244,7 @@ <string name="buzz_system_ttl_set">%1$s beállította, hogy az üzenetek %2$s után tűnjenek el</string> <string name="buzz_system_ttl_cleared">%1$s kikapcsolta az eltűnő üzeneteket</string> <string name="buzz_system_channel_archived">%1$s archiválta ezt a csatornát</string> - <string name="buzz_system_channel_unarchived">%1$s visszaállította ezt a csatornát</string> + <string name="buzz_system_channel_unarchived">%1$s helyreállította ezt a csatornát</string> <string name="buzz_system_channel_created">%1$s létrehozta ezt a csatornát</string> <string name="buzz_system_channel_deleted">%1$s törölte ezt a csatornát</string> <string name="buzz_system_message_deleted">%1$s törölt egy üzenetet</string> @@ -2889,6 +2889,7 @@ <string name="community_rules_violation_wot_gate_failed">Ön nem felel meg a közösség bizalmi hálóra (Web-of-Trust) vonatkozó követelményeinek.</string> <string name="community_rules_violation_stale_rules">A legfrissebb közösségi szabálydokumentum elavultként el lett utasítva.</string> <string name="marmot_groups_title">Marmot csoportok</string> + <string name="cordn_groups_title">Cordn csoportok</string> <string name="marmot_create_group">Csoport létrehozása</string> <string name="marmot_create_group_title">Marmot csoport létrehozása</string> <string name="marmot_create_group_footer">Egy új MLS-csoport jön létre. A tagokat később adhatja hozzá.</string> @@ -3420,7 +3421,7 @@ </plurals> <string name="cashu_quote_confirm">Kifizetsz %1$s satot + legfeljebb %2$s sat díjat?</string> <string name="cashu_settings_delete_confirm_body">Törlési kérés közzététele a(z) %1$s pénzverdéről szóló kind:38000 ajánlásához. Azok az átjátszók, amelyek tiszteletben tartják a NIP-09-et, el fogják távolítani.</string> - <string name="cashu_settings_restore_result">%1$s visszaállíttva %2$s bizonyítékon keresztül.</string> + <string name="cashu_settings_restore_result">%1$s helyrehozva %2$s bizonyítékon keresztül.</string> <string name="cashu_successful_redemption">Cashu megkapva</string> <string name="cashu_successful_redemption_explainer">%1$s satoshi érkezett a pénztárcájába. (Költség: %2$s satoshi)</string> <string name="cashu_unsafe_mint_url">Nem biztonságos Cashu pénzverde-cím</string> @@ -3529,7 +3530,7 @@ <string name="concord_invite_revoked_ok">Meghívási hivatkozás visszavonva.</string> <string name="concord_invite_title">Meghívóhivatkozás</string> <string name="concord_leave_community">Közösség elhagyása</string> - <string name="concord_leave_message">Elhagyja a következőt: %1$s? Eltávolításra kerül ezen fiók listájáról, és leáll a szinkronizálása az Ön eszközein. A közösség nem kap értesítést, és Ön nem kerül le a tagok listájáról. Azok az üzenetek, amelyeket már nem tud visszafejteni, helyreállíthatatlanná válhatnak, és csak új meghívóval térhet vissza.</string> + <string name="concord_leave_message">Elhagyja a következőt: %1$s? Eltávolításra kerül ezen fiók listájáról, és leáll a szinkronizálása az Ön eszközein. A közösség nem kap értesítést, és Ön nem kerül le a tagok listájáról. Azok az üzenetek, amelyeket már nem tud visszafejteni, helyrehozhatatlanná válhatnak, és csak új meghívóval térhet vissza.</string> <string name="concord_leave_owner_warning">Ön hozta létre ezt a közösséget. A kilépés nem törli, és nem is adja át másnak, de eldobja a listáján tárolt tulajdonosi kulcsot — a jövőben nem fogja tudni kezelni.</string> <string name="concord_leave_title">Elhagyja a közösséget?</string> <string name="concord_make_admin">Kinevezés adminisztrátornak</string> @@ -4250,7 +4251,7 @@ <string name="napplets">nKisalkalmazások</string> <string name="nest_chat_send_failed_title">Nem sikerült elküldeni az üzenetet</string> <string name="nest_hush_local">Előadó elnémítása</string> - <string name="nest_hush_local_restore">Ezen előadó visszaállítása</string> + <string name="nest_hush_local_restore">Ezen előadó helyreállítása</string> <string name="nest_leave">Elhagyás</string> <string name="nest_leave_host_close_failed">Nem sikerült a szobát zártként megjelölni. Távozás ettől függetlenül — a szoba automatikusan bezár.</string> <plurals name="nest_listener_count"> @@ -5232,14 +5233,120 @@ <!-- cordn: groups delivered by an MCP coordinator rather than by relays. The exposure strings state what the coordinator operator learns; see spec/00.md §8 and quartz/plans/2026-09-17-cordn-interop.md. --> + <string name="cordn_badge_coordinator">Koordinátor-csoport</string> + <string name="cordn_badge_coordinator_desc">Koordinátor kézbesíti, nem az átjátszók</string> + <string name="cordn_exposure_title">Mit lát ez a koordinátor</string> + <string name="cordn_exposure_subtitle">A koordinátor egyetlen kiszolgáló, amely ennek a csoportnak minden üzenetét sorrendben továbbítja. Ez másfajta kompromisszum, mint az átjátszóké; érdemes utánaolvasnia, mielőtt úgy kezelné ezt, mint bármely más csevegést.</string> + <string name="cordn_exposure_dimension_content">Üzenetek tartalma</string> + <string name="cordn_exposure_dimension_membership">Ki van a csoportban</string> + <string name="cordn_exposure_dimension_messaging">Ki mit küld</string> + <string name="cordn_exposure_level_none">Olvashatatlan</string> + <string name="cordn_exposure_level_pseudonymous">Eldobható kulcs alatt</string> + <string name="cordn_exposure_level_identified">A valódi fiókjához van kötve</string> + <string name="cordn_exposure_details">Részletek</string> + <string name="cordn_exposure_coordinator">%1$s koordinátor</string> + <string name="cordn_exposure_differs">Ez nem ugyanaz a kitettség, mint egy Marmot csoportnál, ahol egyetlen üzemeltető sem birtokolja a teljes beszélgetést.</string> + <string name="cordn_note_membership">A csatlakozás mindkét végén valódi fiókokat nevez meg, így a koordinátor tudja, ki van ebben a csoportban.</string> + <string name="cordn_note_linked">Egyetlen eldobható kulcs küld és kér le minden üzenetet az itteni csoportjához, így a koordinátor megállapíthatja, hogy mindegyik Ön.</string> + <string name="cordn_note_history">Egyetlen üzemeltető birtokolja minden általa továbbított csoport teljes, sorrendbe rendezett előzményét.</string> + <string name="cordn_note_publication">A közzétett kulcscsomag egy aláírt nyilvántartás arról, hogy ez a fiók koordinátort használ, és a koordinátor bárkinek átadhatja.</string> + <string name="cordn_note_sizes">Az üzenetek nincsenek kitöltve, így a koordinátor mindegyiket látja, hogy milyen hosszúak.</string> + <string name="cordn_note_encryption_bug">Ez a kliens nem titkosít a koordinátor felé. Ez egy hiba, jelentse minél hamarabb.</string> + <string name="cordn_health_unknown">Még nem jött létra a kapcsolat</string> + <string name="cordn_health_ok">Válaszol</string> + <string name="cordn_health_down">Nem válaszol</string> + <string name="cordn_link_explainer">Illesszen be egy Önnel megosztott cordn1… hivatkozást, hogy lássa, melyik koordinátor továbbítja azt a csoportot, és mit tudna meg az az üzemeltető Önről, ha csatlakozna.</string> + <string name="cordn_link_field">cordn1…</string> + <string name="cordn_link_inspect">Vizsgálat</string> + <string name="cordn_link_paste">Beillesztés</string> + <string name="cordn_link_clear">Törlés</string> + <string name="cordn_link_invalid">Ez nem egy Cordn csoporthivatkozás: %1$s</string> + <string name="cordn_link_group_id">Csoportazonosító</string> + <string name="cordn_link_coordinator">Koordinátor</string> + <string name="cordn_link_relays">Elérhető ezeken keresztül</string> + <string name="cordn_link_no_coordinator">Ez a hivatkozás nem nevez meg koordinátort, így önmagában nem követhető. Kérdezze meg a küldőt, melyik koordinátor továbbítja a csoportot.</string> + <string name="cordn_link_not_joinable">A hivatkozás vizsgálata nem csatlakozik semmihez, és a birtoklása nem tesz taggá.</string> <!-- cordn entries in the settings catalog and the Messages chat-type list. Here rather than in amethyst/res because both screens read them through Res. --> + <string name="cordn_link_title">Cordn csoporthivatkozás</string> + <string name="cordn_link_search_keywords">cordn koordinátor csoporthivatkozás meghívó mls csevegés metaadat adatvédelem kitettség</string> + <string name="cordn_coordinators_title">Cordn koordinátorok</string> + <string name="cordn_coordinators_search_keywords">cordn koordinátor hozzáadás eltávolítás tisztítás állapot kiszolgáló mls csevegés sorbarendezés</string> + <string name="cordn_keypackages_title">Cordn kulcscsomagok</string> + <string name="cordn_keypackages_search_keywords">cordn kulcscsomag közzététel meghívó végsőmegoldás koordinátor mls</string> + <string name="cordn_backup_title">Cordn biztonsági mentés</string> + <string name="cordn_backup_search_keywords">cordn biztonsági mentés helyreállítás exportálás importálás jelmondat csoportok helyrehozás</string> + <string name="chat_type_cordn_title">Cordn csoportok</string> + <string name="chat_type_cordn_desc">Végpontok között titkosított csoportok, amelyeket az Ön által választott koordinátor rendez, nem pedig átjátszók.</string> <!-- cordn strings on screens main migrated to Res: the shared inbox row and the new-conversation picker. --> + <string name="cordn_group_untitled">%1$s csoport</string> + <string name="cordn_group_no_messages_yet">Még nincsenek üzenetek</string> + <string name="cordn_group_via_coordinator">Cordn csoport, továbbítja: %1$s</string> + <string name="cordn_preview_deleted">Üzenet törölve</string> + <string name="cordn_preview_voice_note">Hangfelvétel</string> + <string name="cordn_preview_photo">Fénykép</string> + <string name="cordn_preview_video">Videó</string> + <string name="cordn_preview_file">Fájl</string> + <string name="new_conversation_cordn_title">Cordn csoport</string> + <string name="new_conversation_cordn_tagline">Titkosított csoportos csevegés, amelyet az Ön által választott koordinátor rendez.</string> + <string name="new_conversation_cordn_chip">Koordinált</string> + <string name="new_conversation_cordn_best">Azoknak a csapatoknak a legjobb, amelyek a beszélgetés egyetlen megbízható sorrendjét szeretnék.</string> + <string name="new_conversation_cordn_cta">Cordn csoport létrehozása</string> + <string name="new_conversation_cordn_pro_1">Végponttól végpontig titkosított MLS-sel – a koordinátor soha nem olvashat el egyetlen üzenetet sem.</string> + <string name="new_conversation_cordn_pro_2">Egy mindenki által elfogadott sorrend, így az előzményeket egy hibás óra sem keverheti össze.</string> + <string name="new_conversation_cordn_con_1">A koordinátor tudja, ki van a csoportban és mikor beszélnek, még ha nem is tudja elolvasni, mit mondanak.</string> + <string name="cordn_invitations_entry">Meghívták egy cordn csoportba?</string> + <string name="cordn_invitations_entry_action">Meghívók áttekintése</string> <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <string name="cordn_hub_title">Cordn</string> + <string name="cordn_hub_explainer">Csoportok, amelyeket az Ön által választott koordinátor kézbesít, nem pedig átjátszók.</string> + <string name="cordn_hub_link">Csoporthivatkozás vizsgálata</string> + <string name="cordn_hub_link_desc">Nézze meg, mire mutat egy cordn1… hivatkozás, mielőtt bármit tenne</string> + <string name="cordn_migrate_section_send">Átköltöztetés erről a telefonról</string> + <string name="cordn_migrate_section_receive">Átköltöztetés egy másik telefonról</string> + <string name="cordn_keypackages_section">Közzétett kulcscsomagok</string> + <string name="cordn_link_section_input">A hivatkozás</string> + <string name="cordn_coordinators_section_yours">Az Ön által használt koordinátorok</string> + <string name="cordn_coordinators_section_discover">Bemutatkozottak</string> + <string name="cordn_coordinators_section_manual">Hozzáadás kézzel</string> + <string name="cordn_backup_section_passphrase">Jelmondat</string> + <string name="cordn_backup_section_export">Biztonsági mentés készítése</string> + <string name="cordn_backup_section_restore">Helyreállítás biztonsági mentésből</string> + <string name="cordn_hub_section_service">A koordinátor</string> + <string name="cordn_hub_section_device">Ez az eszköz</string> + <string name="cordn_hub_coordinators">Koordinátorok</string> + <string name="cordn_hub_coordinators_desc">A kiszolgálók, amelyek a csoportjait továbbítják</string> + <string name="cordn_hub_keypackages">Kulcscsomagok</string> + <string name="cordn_hub_keypackages_desc">Ez teszi lehetővé, hogy mások csoportokhoz adhassák Önt</string> + <string name="cordn_hub_backup">Biztonsági mentés és helyreállítás</string> + <string name="cordn_hub_backup_desc">Egy jelmondattal titkosított fájl, amelyet Ön őriz</string> + <string name="cordn_hub_migrate">Átköltöztetés másik telefonra</string> + <string name="cordn_hub_migrate_desc">Költöztesse át a csoportjait arra az eszközre, amelyre vált</string> + <string name="cordn_migrate_title">Átköltöztetés egy másik eszközre</string> + <string name="cordn_migrate_explainer">A cordn csoportjai ezen az eszközön élnek. Az átköltöztetéshez egyetlen kód kell: ez az eszköz közzéteszi őket, és a másik eszközön beolvashatja őket, majd ez az eszköz nem tárolja tovább.</string> + <string name="cordn_migrate_sign_in_first">Először jelentkezzen be ugyanazzal a fiókkal a másik eszközön. Ez a csoportjait költözteti át, és nem a kulcsát.</string> + <string name="cordn_migrate_export">Átköltöztetés indítása</string> + <string name="cordn_migrate_exporting">Közzététel…</string> + <string name="cordn_migrate_scan_this">Olvassa be ezt a másik eszközön</string> + <string name="cordn_migrate_code_note">A kód csak egy mutató. Aki lefényképezi, semmit sem tud meg, amire mutat, az az Ön fiókjához van titkosítva.</string> + <string name="cordn_migrate_handed_off">Ez az eszköz átköltöztette a csoportjait</string> + <string name="cordn_migrate_handed_off_desc">Leállította az átköltöztetést, mert a két eszköz, amely ugyanabból a csoportból küld, mindenki számára tönkretenné az üzenetek fogadását. A csoportjai még mindig ezen az eszközön vannak, és semmi sem törlődött.</string> + <string name="cordn_migrate_cancel">Eszköz visszavétele</string> + <string name="cordn_migrate_cancel_desc">Ezt akkor használja, ha az átköltöztetés nem fejeződött be. Csak akkor tegye, ha a másik eszközön még nem kezdett el üzeneteket küldeni.</string> + <string name="cordn_migrate_receive">Fogadás egy másik eszközről</string> + <string name="cordn_migrate_scan">Kód beolvasása</string> + <string name="cordn_migrate_paste">vagy illessze be</string> + <string name="cordn_migrate_import">Csoportok átköltöztetése ide</string> + <string name="cordn_migrate_importing">Lekérés…</string> + <string name="cordn_migrate_replaces_warning">Ez lecseréli az eszközön már meglévő cordn csoportokat. Ezek nem olvaszthatók egybe.</string> + <string name="cordn_migrate_no_groups">Nincsenek átköltöztethető cordn csoportok ezen az eszközön.</string> + <string name="cordn_migrate_no_server">Először állítson be médiakiszolgálót, a titkosított dokumentumoknak valahol lenniük kell, amíg a másik telefon le nem kéri őket.</string> + <string name="cordn_migrate_exposure_title">Mi hagyja el ezt az eszközt</string> + <string name="cordn_migrate_exposure_body">A csoport állapota titkosítva van, és fel lesz töltve a médiakiszolgálójára, hogy a másik eszköz le tudja kérni. Az a kiszolgáló számára olvashatatlan, csak egy méretet és egy időpontot lát. Ha nem szeretne semmit hátrahagyni, utána törölje a blobokat.</string> <string name="sno_object_title">3D-objektum</string> <string name="sno_object_details">%1$d csúcs · %2$d lap</string> <string name="sno_object_unreadable">Ezt a 3D-objektumot nem sikerült beolvasni (%1$s szabály)</string> @@ -5283,18 +5390,124 @@ <item quantity="other">%1$d bájt valamiből, amit ez a kliens nem olvas</item> </plurals> <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> + <string name="browser_pill_close">Bezárás</string> + <string name="browser_pill_tile_copy">Hivatkozás másolása</string> + <string name="browser_pill_tile_find">Keresés</string> + <string name="browser_pill_tile_text">Szövegméret</string> + <string name="browser_pill_tile_desktop">Asztali</string> + <string name="browser_pill_tile_home">Hozzáadás a kezdőképernyőhöz</string> + <string name="browser_pill_tile_other">Másik böngésző</string> + <string name="browser_pill_tile_full">Teljes képernyő</string> + <string name="browser_pill_edit_address">Cím szerkesztése</string> + <string name="browser_pill_back">Vissza</string> + <string name="browser_pill_forward">Előre</string> + <string name="browser_pill_reload">Újratöltés</string> + <string name="browser_pill_stop">Betöltés leállítása</string> + <string name="browser_pill_favorite_add">Hozzáadás a kedvencekhez</string> + <string name="browser_pill_favorite_remove">Eltávolítás a kedvencekből</string> + <string name="browser_pill_share">Megosztás</string> + <string name="browser_pill_copy">Hivatkozás másolása</string> + <string name="browser_pill_find">Keresés az oldalon</string> + <string name="browser_pill_text_size">Szövegméret</string> + <string name="browser_pill_add_home">Hozzáadás a kezdőképernyőhöz</string> + <string name="browser_pill_desktop">Asztali oldal</string> + <string name="browser_pill_other_browser">Megnyitás böngészőben</string> + <string name="browser_pill_other_browser_named">Megnyitás a(z) %1$s alkalmazásban</string> + <string name="browser_pill_full_screen">Teljes képernyő</string> + <string name="browser_pill_left_site">Elhagyta a(z) %1$s oldalt</string> + <string name="browser_pill_back_to_app">Vissza az alkalmazásba</string> + <string name="browser_pill_text_smaller">Kisebb szöveg</string> + <string name="browser_pill_text_larger">Nagyobb szöveg</string> + <string name="browser_pill_text_reset">Visszaállítás</string> + <string name="browser_pill_text_value">%1$d%</string> + <string name="browser_pill_privacy">Adatvédelem</string> + <string name="browser_pill_tor_title">Onion-útválasztás</string> + <string name="browser_pill_tor_on">Az oldal nem látja az Ön IP-címét</string> + <string name="browser_pill_tor_off">Az oldal látja az Ön IP-címét</string> + <string name="browser_pill_site_settings">Oldalbeállítások</string> + <string name="browser_pill_site_settings_none">Még semmi sincs engedélyezve</string> + <string name="browser_pill_access">Mihez férhet hozzá</string> + <string name="browser_pill_access_desc">A kulcsai soha nem hagyják el az Amethystet</string> + <string name="browser_pill_access_keys_desc">Minden aláírás, közzététel, feltöltés vagy fizetés az Ön jóváhagyását követeli meg</string> + <string name="browser_pill_access_none">Nincs különleges hozzáférés</string> + <string name="browser_pill_access_none_desc">Homokozóban fut, és nem tud cselekedni a fiókjában</string> + <string name="browser_pill_access_manage">Engedélyek kezelése</string> + <string name="browser_pill_access_nsite">nOldal · homokozóban</string> + <string name="browser_pill_access_napplet">nKisalkalmazás · homokozóban</string> + <string name="browser_pill_console">Konzol</string> + <string name="browser_pill_security_https">Biztonságos kapcsolat</string> + <string name="browser_pill_security_http">Nem biztonságos</string> + <string name="browser_pill_security_tor">Onion-útvonalon</string> + <string name="browser_pill_security_sandbox">Homokozóban futó alkalmazás</string> + <string name="browser_pill_permission_camera">Kamera</string> + <string name="browser_pill_permission_microphone">Mikrofon</string> + <string name="browser_pill_permission_location">Helyzet</string> + <string name="browser_pill_decision_ask">Rákérdezés</string> + <string name="browser_pill_decision_allow">Engedélyezés</string> + <string name="browser_pill_decision_block">Blokkolás</string> + <string name="browser_pill_permission_state">%1$s %2$s</string> + <string name="browser_pill_decision_allowed">engedélyezve</string> + <string name="browser_pill_decision_blocked">blokkolva</string> + <string name="browser_pill_address_hint">Keresés vagy cím megadása</string> + <string name="browser_pill_go">Ugrás</string> + <string name="browser_pill_clear">Törlés</string> + <string name="browser_pill_paste_go">Beillesztés és ugrás</string> + <string name="browser_pill_fill_in">Cím használata</string> + <string name="browser_pill_find_hint">Keresés az oldalon</string> + <string name="browser_pill_find_count">%1$d / %2$d</string> + <string name="browser_pill_find_none">Nincs találat</string> + <string name="browser_pill_find_previous">Előző találat</string> + <string name="browser_pill_find_next">Következő találat</string> + <string name="browser_pill_find_close">Keresés bezárása az oldalon</string> + <string name="browser_pill_console_all">Összes</string> + <string name="browser_pill_console_errors">Hibák</string> + <string name="browser_pill_console_warnings">Figyelmeztetések</string> + <string name="browser_pill_console_copy">Másolás</string> + <string name="browser_pill_console_clear">Törlés</string> + <string name="browser_pill_console_empty">Még semmi sincs naplózva</string> + <string name="browser_pill_perm_title">Ez az oldal használni akarja</string> + <string name="browser_pill_perm_camera_desc">Nézze meg, amit a kamerája lát</string> + <string name="browser_pill_perm_microphone_desc">Hallgassa meg, amit a mikrofonja hall</string> + <string name="browser_pill_perm_location_desc">Nagyjából tudja, hol van</string> + <string name="browser_pill_perm_tor_note">A hívások Tor-on keresztül is felfedhetik az IP-címét.</string> + <string name="browser_pill_perm_allow">Engedélyezés a látogatás idejére</string> + <string name="browser_pill_perm_once">Csak most az egyszer</string> + <string name="browser_pill_perm_deny">Ne engedélyezze</string> + <string name="browser_pill_dialog_says">%1$s üzeni</string> + <string name="browser_pill_dialog_generic">Ez az oldal üzeni</string> + <string name="browser_pill_dialog_block">Oldal megakadályozása a még több párbeszédablak megjelenítésében</string> + <string name="browser_pill_dialog_answer">Az Ön válasza</string> + <string name="browser_pill_dialog_leave_title">Elhagyja az oldalt?</string> + <string name="browser_pill_dialog_leave_message">Lehet, hogy a módosításai nem lettek mentve.</string> + <string name="browser_pill_dialog_leave">Elhagyás</string> + <string name="browser_pill_ok">OK</string> + <string name="browser_pill_cancel">Mégse</string> + <string name="browser_pill_info_connection">Kapcsolódás</string> + <string name="browser_pill_info_https">A kapcsolat titkosított</string> + <string name="browser_pill_info_https_desc">Az Ön által küldött információ útközben nem olvasható.</string> + <string name="browser_pill_info_http">A kapcsolat nincs titkosítva</string> + <string name="browser_pill_info_http_desc">Ne adjon meg jelszavakat vagy fizetési adatokat.</string> + <string name="browser_pill_info_tor">Onion-útvonalon Tor-on keresztül</string> + <string name="browser_pill_info_open">Nyílt web</string> + <string name="browser_pill_info_certificate">Tanúsítvány</string> + <string name="browser_pill_info_certificate_desc">%2$s állította ki %1$s részére · érvényes eddig: %3$s</string> + <string name="browser_pill_info_permissions">Engedélyek</string> + <string name="browser_pill_info_site_data">Sütik és oldaladatok</string> + <string name="browser_pill_info_site_data_desc">Bejelentkezések és beállítások, amelyeket ez az oldal ehhez a fiókhoz mentett</string> + <string name="browser_pill_info_clear">Oldaladatok törlése</string> + <string name="browser_pill_info_clear_confirm">Kijelentkezik erről az oldalról és törli az adatait?</string> <string name="backup_action_block_again">%1$s újbóli letiltása</string> <string name="backup_action_keep_count">%1$s megtartása</string> <string name="backup_action_rejoin">Újbóli csatlakozás ehhez: %1$s</string> <string name="backup_action_remute">%1$s újbóli némítása</string> - <string name="backup_action_restore_count">%1$s visszaállítása</string> - <string name="backup_action_restore_feeds">Hírfolyamok visszaállítása</string> - <string name="backup_action_restore_places">Helyek visszaállítása</string> - <string name="backup_action_restore_profile">Profil visszaállítása</string> - <string name="backup_action_restore_providers">Szolgáltatók visszaállítása</string> - <string name="backup_action_restore_relays">Átjátszók visszaállítása</string> - <string name="backup_action_restore_topics">Témák visszaállítása</string> - <string name="backup_action_restore_wallet">Pénztárca visszaállítása</string> + <string name="backup_action_restore_count">%1$s helyreállítása</string> + <string name="backup_action_restore_feeds">Hírfolyamok helyreállítása</string> + <string name="backup_action_restore_places">Helyek helyreállítása</string> + <string name="backup_action_restore_profile">Profil helyreállítása</string> + <string name="backup_action_restore_providers">Szolgáltatók helyreállítása</string> + <string name="backup_action_restore_relays">Átjátszók helyreállítása</string> + <string name="backup_action_restore_topics">Témák helyreállítása</string> + <string name="backup_action_restore_wallet">Pénztárca helyreállítása</string> <string name="backup_card_accept">Elfogadás</string> <plurals name="backup_card_and_added"> <item quantity="one">%1$s és %2$d hozzáadva</item> @@ -5428,7 +5641,7 @@ <string name="backup_conflict_relay_read_write">olvasás és írás</string> <string name="backup_conflict_relay_write_only">csak írható</string> <string name="backup_conflict_removed">Eltávolítva (%1$s)</string> - <string name="backup_conflict_restore_mine">Mentett verzió visszaállítása</string> + <string name="backup_conflict_restore_mine">Mentett verzió helyreállítása</string> <string name="backup_conflict_title">Ön módosította a következőt: %1$s, egy másik alkalmazásban</string> <string name="backup_entry_algo_feed">Hírfolyamok</string> <string name="backup_entry_chat_room">Csevegőszobák</string> @@ -5607,4 +5820,252 @@ <string name="backup_type_trust_providers_explainer">A szolgáltatások, amelyekben megbízik, hogy rangsorolják és ellenőrizzék Önnek az embereket.</string> <string name="backup_type_trusted_relays">megbízható átjátszólista</string> <string name="backup_type_trusted_relays_explainer">Átjátszók, amelyekhez kérdezés nélkül kapcsolódhat.</string> + <string name="cordn_action_delete">Törlés</string> + <string name="cordn_action_edit">Szerkesztés</string> + <string name="cordn_action_editing">Üzenet szerkesztése</string> + <string name="cordn_action_go_to_message">Ugrás az üzenetre</string> + <string name="cordn_action_pin">Kitűzés</string> + <string name="cordn_action_reply">Válasz</string> + <string name="cordn_action_unpin">Kitűzés megszüntetése</string> + <string name="cordn_admin_action_failed">Nem sikerült végrehajtani a módosítás.</string> + <string name="cordn_admin_coordinator_silent">A koordinátor nem válaszolt, így semmi sem változott. Próbálja újra.</string> + <string name="cordn_admin_no_key_package">%1$s nem tett közzé kulcscsomagot ehhez a koordinátorhoz, így még nem adható hozzá. Kérje meg, hogy először nyissa meg a cordnt ezen a koordinátoron.</string> + <string name="cordn_admin_no_self_remove">Nem távolíthatja el saját magát egy cordn csoportból.</string> + <string name="cordn_admin_no_welcome">%1$s számára nem sikerült utat biztosítani a csoportba, így a hozzáadása megszakadt.</string> + <string name="cordn_admin_not_a_member">%1$s nincs ebben a csoportban.</string> + <string name="cordn_admin_someone">Az a személy</string> + <string name="cordn_admin_wrong_key_package">Ez a koordinátor egy máshoz tartozó kulcscsomagot szolgáltatott, így %1$s nem lett hozzáadva. Semmi sem változott.</string> + <string name="cordn_backup_contents_body">A csoport titkosítási kulcsai, a kulcscsomagjai és azokban a csoportokban lévő minden üzenet. Aki megnyitja, elolvashatja a beszélgetéseket. A jelmondat az egyetlen dolog, ami védi, miután elhagyja az Amethystet.</string> + <string name="cordn_backup_contents_title">Mi van a fájlban</string> + <string name="cordn_backup_explainer">Az nsec nem tudja visszahozni a cordn csoportokat. Egy cordn csoport titkosítási állapotként él ezen az eszközön, és sorrendbe rendezett adatfolyamként egy koordinátoron, amely nem tudja elolvasni; ha biztonsági mentés nélkül elveszíti az eszközt, a csoport eltűnik, beleértve mindazt, ami már elhangzott benne.</string> + <string name="cordn_backup_export">Exportálás</string> + <string name="cordn_backup_exported">Biztonsági mentés kész.</string> + <string name="cordn_backup_failed">Ez nem működött. Ellenőrizze a jelmondatot és a fájlt.</string> + <string name="cordn_backup_passphrase">Jelmondat</string> + <string name="cordn_backup_restore">Helyreállítás</string> + <string name="cordn_backup_restore_body">A helyreállítás lecseréli az ezen az eszközön lévő cordn csoportokat a fájlban lévőkre. Ez egy olyan eszköznek szól, amely átvett egy másikat; nem arra való, hogy ugyanazokat a csoportokat egyszerre két eszközön használja.</string> + <string name="cordn_backup_restore_confirm_body">Minden most ezen az eszközön lévő cordn csoport eltávolításra és a fájlban lévőkkel lesz helyettesítve.\n\nNe állítsa helyre egy második eszközre, amíg az elsőt még használja. Mindkettő ugyanazokat a csoportkulcsokat birtokolná, és miután mindkettő küldött egy üzenetet, a csoport mindenki számára végleg használhatatlanná válna.</string> + <string name="cordn_backup_restore_confirm_title">Lecseréli az eszköz cordn csoportjait?</string> + <string name="cordn_backup_restore_title">Helyreállítás</string> + <string name="cordn_backup_restored">Helyreállítva.</string> + <string name="cordn_chat_empty_description">Minden ide küldött üzenet titkosítva van ehhez a csoporthoz. A koordinátor továbbítja anélkül, hogy el tudná olvasni.</string> + <string name="cordn_chat_empty_title">Még nincsenek üzenetek</string> + <string name="cordn_chat_unread_divider">Új üzenetek</string> + <string name="cordn_chat_yesterday">Tegnap</string> + <string name="cordn_composer_hint">Üzenet</string> + <string name="cordn_coordinators_add_action">Hozzáadás</string> + <string name="cordn_coordinators_add_failed">Nem sikerült megnyitni azt a koordinátort.</string> + <string name="cordn_coordinators_copy_key">A koordinátor npub-jának másolása</string> + <string name="cordn_coordinators_discover_action">Koordinátorok keresése</string> + <string name="cordn_coordinators_discover_add">Hozzáadás</string> + <string name="cordn_coordinators_discover_explainer">Az átjátszói által már továbbított bejelentéseket olvassa. Egyetlen koordinátorral sem lép kapcsolatba, így egyik sem tudja meg, hogy Ön keresett.</string> + <string name="cordn_coordinators_discover_failed">Nem sikerült beolvasni a bejelentéseket</string> + <string name="cordn_coordinators_discover_none">Senki sem jelentkezik be az átjátszóin.</string> + <string name="cordn_coordinators_discover_seen">Bejelentve ennyi ideje: %1$s</string> + <string name="cordn_coordinators_discover_seen_on">Utoljára bejelentve: %1$s</string> + <string name="cordn_coordinators_explainer">Egy koordinátor egy csoport üzeneteit rendezi sorba, és ez az egyetlen, ami ezt megteheti. Ha elvész, a sorrend is elvész, és egy második nem tükrözi az elsőt; így ezek névvel ellátott dolgok, amelyeket Ön választ, nem pedig felcserélhető átjátszók.</string> + <string name="cordn_coordinators_health_ok">Válaszol.</string> + <string name="cordn_coordinators_health_retrying">Egy hívás sikertelen volt; újrapróbálkozás.</string> + <string name="cordn_coordinators_health_unknown">Még semmit sem kérdeztek tőle.</string> + <string name="cordn_coordinators_hide_older">A bejelentést abbahagyók elrejtése</string> + <string name="cordn_coordinators_identify">Kérdezze meg, ki az</string> + <string name="cordn_coordinators_key">Koordinátorkulcs</string> + <string name="cordn_coordinators_label">Egy név kell neki (az Öné, nem kötelező)</string> + <string name="cordn_coordinators_label_note">Ez a név az Öné, és ezen az eszközön marad. Egy koordinátor nem tudja bizonyítani a nevét, ezért soha nem kérdeznek tőle ilyet.</string> + <string name="cordn_coordinators_more">További műveletek</string> + <string name="cordn_coordinators_none">Még nincsenek koordinátorok.</string> + <string name="cordn_coordinators_purge">Tisztítás</string> + <string name="cordn_coordinators_purge_body">Az eltávolítás megtartja a csoportokat ezen az eszközön, így a koordinátor újbóli hozzáadása visszahozza őket. A tisztítás törli őket: a csoportkulcsok, az olvasási pozíciók és a kulcscsomagjai mind eltűnnek, és ezekbe a csoportokba csak friss meghívással lehet újra belépni.\n\nA koordinátort nem értesítik, és megtartja mindazt, amije már volt.</string> + <string name="cordn_coordinators_purge_title">Tisztítja ezt a koordinátort?</string> + <string name="cordn_coordinators_relays">%1$s</string> + <string name="cordn_coordinators_relays_label">Válaszol ezeken</string> + <string name="cordn_coordinators_relays_more">%1$s és további %2$d</string> + <string name="cordn_coordinators_remove">Eltávolítás</string> + <string name="cordn_coordinators_rename">Átnevezés</string> + <string name="cordn_coordinators_server">Azt mondja, ő: %1$s</string> + <string name="cordn_coordinators_server_claim">Egy koordinátor neve és verziója az általa választott szöveg. Az egyetlen dolog, ami azonosítja, a nyilvános kulcsa.</string> + <string name="cordn_coordinators_server_silent">Válaszolt, de semmit sem nevezett meg.</string> + <string name="cordn_coordinators_show_all">Mind a(z) %1$d megjelenítése</string> + <string name="cordn_coordinators_show_fewer">Kevesebb megjelenítése</string> + <string name="cordn_coordinators_stale_note">Ezek több mint egy hónapja nem jelentkeztek be. A csoport létrehozása egy eltűnt koordinátoron sikertelen lesz.</string> + <string name="cordn_create_action">Csoport létrehozása</string> + <plurals name="cordn_create_action_invite"> + <item quantity="one">Létrehozás és %1$d személy meghívása</item> + <item quantity="other">Létrehozás és %1$d személy meghívása</item> + </plurals> + <string name="cordn_create_add_member">Valaki hozzáadása</string> + <string name="cordn_create_admin">Adminisztrátor</string> + <plurals name="cordn_create_admin_count"> + <item quantity="one">%1$d személy adhat hozzá és távolíthat el</item> + <item quantity="other">%1$d személy adhat hozzá és távolíthat el</item> + </plurals> + <string name="cordn_create_admin_only_me">Csak én kezelem ezt a csoportot</string> + <string name="cordn_create_admin_only_me_on">Csak Ön hívhat meg, távolíthat el tagokat vagy változtathatja meg a csoport adatait. Ezt később átadhatja valaki másnak.</string> + <string name="cordn_create_coordinator">Koordinátor</string> + <string name="cordn_create_coordinator_change">Módosítás</string> + <string name="cordn_create_coordinator_discover">Koordinátorok keresése</string> + <string name="cordn_create_coordinator_hide">Kész</string> + <string name="cordn_create_coordinator_new">Egy másik koordinátor</string> + <string name="cordn_create_coordinator_pubkey">Koordinátor nyilvános kulcsa (hex vagy npub)</string> + <string name="cordn_create_coordinator_relays">Átjátszók, amelyeken válaszol, soronként egy</string> + <string name="cordn_create_coverage_partial">%2$d / %1$d embert érne el, a többi kimaradna</string> + <string name="cordn_create_coverage_unknown">Nincs ellenőrizve; a rendszer rá fog kérdezni a kiválasztására</string> + <string name="cordn_create_description">Leírás (nem kötelező)</string> + <string name="cordn_create_egalitarian">Mindenki hozzáadhat és eltávolíthat</string> + <string name="cordn_create_egalitarian_note">A csoport minden tagja meghívhat, eltávolíthat tagokat és megváltoztathatja a csoport adatait. Bármely tag később kinevezhet adminisztrátorokat, ami ezt megszünteti.</string> + <string name="cordn_create_explainer">Egy cordn csoportot egy Ön által választott koordinátor rendez. A csoport létrehozása ezen az eszközön történik – a koordinátor csak akkor szerez tudomást a csoport létezéséről, amikor meghív valakit vagy üzenetet küld.</string> + <string name="cordn_create_failed">Nem sikerült létrehozni a csoportot.</string> + <string name="cordn_create_name">Csoportnév</string> + <string name="cordn_create_no_coordinator">Nincs koordinátor kiválasztva</string> + <string name="cordn_create_outcome_explainer">Minden meghívó egy Üdvözlés is, amelyet a koordinátoron hagynak, így egyenként sikertelenek lehetnek. Senkit sincs értesítve: az emberek legközelebb akkor látják, amikor megnyitják az alkalmazást.</string> + <string name="cordn_create_outcome_failed">A koordinátor elutasította a meghívót</string> + <string name="cordn_create_outcome_link">Hivatkozás küldése</string> + <string name="cordn_create_outcome_no_welcome">Hozzáadva a csoporthoz, de a meghívó nem érte el, próbálja újra a csoport képernyőjéről</string> + <string name="cordn_create_outcome_open">Csevegés megnyitása</string> + <string name="cordn_create_outcome_skipped">Itt nincs kulcs, így nem lehetett Üdvözlést hagyni</string> + <string name="cordn_create_outcome_title">A csoport létezik</string> + <string name="cordn_create_outcome_waiting">Várakozás rájuk</string> + <string name="cordn_create_people_done">Kész</string> + <string name="cordn_create_people_empty">Még senki sincs hozzáadva. Egy csoport létrehozható üresen, és a meghívók később küldhetők, de ha most megnevezi az embereket, az mutatja meg, melyik koordinátor tudja elérni őket.</string> + <string name="cordn_create_people_none">Még senki – koppintson az emberek hozzáadásához</string> + <string name="cordn_create_people_note">Ha most megnevezi az embereket, akkor mondja meg, melyik koordinátor tudja őket elérni. Mindegyikük kap egy Üdvözlést azon a koordinátoron, amelyet legközelebb akkor választanak ki, amikor megnyitják az alkalmazást.</string> + <plurals name="cordn_create_reach_count"> + <item quantity="one">Elérhető %1$d koordinátoron</item> + <item quantity="other">Elérhető %1$d koordinátoron</item> + </plurals> + <string name="cordn_create_reach_none">Nincs kulcs egyetlen Ön által használt koordinátoron sem</string> + <string name="cordn_create_reach_unknown">Még nincs ellenőrizve</string> + <string name="cordn_create_step_admins">Ki adhat hozzá és távolíthat el</string> + <string name="cordn_create_step_name">Nevezze el</string> + <string name="cordn_create_step_people">Ki van benne</string> + <string name="cordn_create_step_where">Hol él</string> + <string name="cordn_create_title">Új Cordn csoport</string> + <plurals name="cordn_create_unreachable_count"> + <item quantity="one">%1$d személyt egyetlen Ön által használt koordinátor sem tud elérni</item> + <item quantity="other">%1$d személyt egyetlen Ön által használt koordinátor sem tud elérni</item> + </plurals> + <string name="cordn_create_you">Ön, mindig adminisztrátor</string> + <string name="cordn_delete_confirm_body">A csoport minden tagja visszavontként fogja látni ezt az üzenetet, és az Amethyst nem fogja többé megjeleníteni a szövegét. A koordinátor továbbra is birtokolja a titkosított másolatot, amelyként elküldték, és aki már elolvasta, az már elolvasta.</string> + <string name="cordn_delete_confirm_title">Üzenet visszavonása</string> + <string name="cordn_delivery_accepted">A koordinátor elfogadta</string> + <string name="cordn_exposure_close">Értem</string> + <string name="cordn_exposure_open">Mit lát ez a koordinátor</string> + <string name="cordn_group_info">Csoportadatok</string> + <string name="cordn_group_unavailable">Ez a csoport nem érhető el. Lehet, hogy a koordinátorát eltávolították.</string> + <string name="cordn_group_unavailable_detail">Adjon hozzá egy koordinátort a Beállításokban, hogy elkezdje használni a cordn csoportokat.</string> + <plurals name="cordn_groups_count"> + <item quantity="one">%1$d csoport</item> + <item quantity="other">%1$d csoport</item> + </plurals> + <string name="cordn_groups_down_note">Ezekből a csoportokból semmi sem érkezik. Az Amethyst folyamatosan újrapróbálkozik.</string> + <string name="cordn_groups_manage">Koordinátorok</string> + <string name="cordn_groups_none">Még nincsenek cordn csoportok</string> + <string name="cordn_groups_none_detail">Egy cordn csoport egyetlen koordinátoron él, amely nyilvántartja a tagságát és továbbítja az üzeneteket, anélkül, hogy valaha is látná, mit írnak.</string> + <string name="cordn_groups_start">Indítson egyet</string> + <string name="cordn_info_add_member">Valaki hozzáadása</string> + <string name="cordn_info_add_member_none">Senkit sem találtunk azon a néven.</string> + <string name="cordn_info_add_member_note">Csak akkor adhatók hozzá, ha közzétettek egy kulcscsomagot ehhez a koordinátorhoz.</string> + <string name="cordn_info_add_member_placeholder">Név, npub vagy nev@domain</string> + <string name="cordn_info_admin_badge">Adminisztrátor</string> + <plurals name="cordn_info_admins_kept"> + <item quantity="one">A csoport %1$d adminisztrátora marad, ahogy van.</item> + <item quantity="other">A csoport %1$d adminisztrátora marad, ahogy van.</item> + </plurals> + <string name="cordn_info_coordinator">Koordinátor</string> + <string name="cordn_info_coordinator_key">Koordinátorkulcs</string> + <string name="cordn_info_coordinator_nprofile">Koordinátorhivatkozás</string> + <string name="cordn_info_copy_nprofile">A koordinátor nprofile-jának másolása</string> + <string name="cordn_info_edit_details">Adatok szerkesztése</string> + <string name="cordn_info_egalitarian">Ennek a csoportnak nincsenek adminisztrátorai: mindenki hozzáadhat, eltávolíthat és átnevezhet, véglegesen.</string> + <string name="cordn_info_epoch">Korszak</string> + <string name="cordn_info_gid">Csoportazonosító</string> + <string name="cordn_info_has_key_package">Hozzáadható ezen a koordinátoron</string> + <string name="cordn_info_members">Tagok</string> + <string name="cordn_info_remove_confirm_body">%1$s nem fog többé üzeneteket fogadni innen. Megtartja, amit már elolvasott, és ez nem vonható vissza – újra meg kellene hívni.</string> + <string name="cordn_info_remove_confirm_title">Eltávolítja a csoportból?</string> + <string name="cordn_info_remove_member">Eltávolítás</string> + <string name="cordn_info_save">Mentés</string> + <string name="cordn_info_technical">Technikai részletek</string> + <string name="cordn_invitations_accept">Csatlakozás</string> + <string name="cordn_invitations_decline">Elutasítás</string> + <string name="cordn_invitations_decline_warning">Az elutasítás végleges. A visszakerüléshez újra meg kell hívni.</string> + <string name="cordn_invitations_explainer">Akkor ellenőrzi, amikor megnyitja ezt a képernyőt, és máskor soha, ha megkérdez egy koordinátort, hogy meghívta-e valaki, az elárulja neki, hogy Ön itt van, ezért az Amethyst nem teszi ezt a háttérben.</string> + <string name="cordn_invitations_load_failed">Nem sikerült beolvasni a meghívókat.</string> + <plurals name="cordn_invitations_members"> + <item quantity="one">%1$d személy már tagja ennek a csoportnak</item> + <item quantity="other">%1$d személy már tagja ennek a csoportnak</item> + </plurals> + <string name="cordn_invitations_members_more">+%1$d további</string> + <string name="cordn_invitations_no_coordinators">Nincs nyitott koordinátora. A cordn meghívók egy koordinátoron keresztül érkeznek, így még nincs hol keresni.</string> + <string name="cordn_invitations_none">Nincs várakozó meghívó.</string> + <string name="cordn_invitations_refresh">Újraellenőrzés</string> + <string name="cordn_invitations_skipped">Egy meghívó egy másik eszköznek maradt</string> + <string name="cordn_invitations_title">Cordn meghívók</string> + <string name="cordn_invitations_unreachable">%1$s nem válaszolt</string> + <string name="cordn_invitations_via">%1$s által</string> + <string name="cordn_keypackages_disclosure">A közzététel egy bejegyzést ír alá a saját fiókkulcsával. Az a koordinátor ezután tudja, hogy ez a fiók létezik és meghívható, véglegesen, függetlenül attól, hogy meghívja-e valaki.</string> + <string name="cordn_keypackages_explainer">Mások csak úgy adhatják hozzá egy cordn csoporthoz, ha elvesznek egy kulcscsomagot, amelyet Ön közzétett annál a koordinátornál. A cordnnak nincs más helye, ahol tárolhatna egyet, így ha egy sincs közzétéve, senki sem tudja meghívni – és semmi sem mondja meg nekik, miért.</string> + <string name="cordn_keypackages_failed">Nem sikerült beolvasni a kulcscsomagokat.</string> + <string name="cordn_keypackages_last_resort_no">Nincsenek tartalékcsomagok. Amint az egyszer használatosak elfogynak, a meghívók sikertelenek lesznek.</string> + <string name="cordn_keypackages_last_resort_yes">Egy végső megoldás csomag közzé van téve, így a meghívó akkor is elkészíthető, ha az egyszer használatosak elfogytak.</string> + <plurals name="cordn_keypackages_orphans"> + <item quantity="one">%1$d csomag, amelyet ez az eszköz nem tud megnyitni</item> + <item quantity="other">%1$d csomag, amelyet ez az eszköz nem tud megnyitni</item> + </plurals> + <string name="cordn_keypackages_orphans_body">A koordinátor odaadja ezeket bárkinek, aki meghívja Önt, de a privát fele nincs ezen az eszközön – egy másik telepítéshez tartozik. Ha az a telepítés eltűnt, egy ilyen használatával készült meghívó olyan welcome-ot hoz létre, amelyet soha senki sem tud megnyitni.</string> + <string name="cordn_keypackages_publish">Egy közzététele</string> + <string name="cordn_keypackages_publish_last_resort">Végső megoldás közzététele</string> + <plurals name="cordn_keypackages_summary"> + <item quantity="one">%1$d egyszer használatos csomag elérhető.</item> + <item quantity="other">%1$d egyszer használatos csomag elérhető.</item> + </plurals> + <string name="cordn_keypackages_topup_note">Miután itt közzétett, az Amethyst csendben pótolja a csomagokat, ahogy elfogynak. Az elsőt soha nem teszi közzé Ön helyett.</string> + <string name="cordn_keypackages_withdraw_all">Összes visszavonása</string> + <string name="cordn_keypackages_withdraw_orphans">Azok visszavonása</string> + <string name="cordn_link_request">Csatlakozási kérelem ehhez a csoporthoz</string> + <string name="cordn_link_request_disclosure">Ez közzétesz egy kulcscsomagot a saját fiókkulcsa alatt, és megmondja a koordinátornak, hogy be szeretne kerülni ebbe a csoportba – függetlenül attól, hogy válaszol-e valaki.</string> + <string name="cordn_link_request_failed">Nem sikerült csatlakozási kérelmet küldeni.</string> + <string name="cordn_link_request_sent">Elküldve. A csoport egyik tagjának hozzá kell adnia Önt; a meghívó a cordn meghívók alatt fog megjelenni.</string> + <string name="cordn_link_scan">Beolvasás</string> + <string name="cordn_media_download_failed">Nem sikerült megnyitni azt a fájlt.</string> + <string name="cordn_media_no_server">Nincs médiakiszolgáló beállítva ehhez a fiókhoz, így nincs hová tenni a fájlt. Válasszon egyet a Beállításokban.</string> + <string name="cordn_media_unreadable">Azt a fájlt nem sikerült beolvasni.</string> + <string name="cordn_media_upload_failed">Nem sikerült elküldeni azt a fájlt.</string> + <plurals name="cordn_member_count"> + <item quantity="one">%1$d tag</item> + <item quantity="other">%1$d tag</item> + </plurals> + <string name="cordn_message_deleted">Üzenet törölve</string> + <string name="cordn_message_details_coordinator">Koordinátor</string> + <string name="cordn_message_details_cursor">Kurzor</string> + <string name="cordn_message_details_sent_at">Elküldve</string> + <string name="cordn_message_details_title">Üzenet részletei</string> + <string name="cordn_message_edited">szerkesztve</string> + <string name="cordn_pinned_by">%1$s rögzítette</string> + <plurals name="cordn_pinned_count"> + <item quantity="one">%1$d rögzített üzenet ebben a csoportban</item> + <item quantity="other">%1$d rögzített üzenet ebben a csoportban</item> + </plurals> + <string name="cordn_pinned_next">Következő rögzített üzenet</string> + <string name="cordn_pinned_previous">Előző rögzített üzenet</string> + <string name="cordn_pinned_show_all">Összes rögzített üzenet megjelenítése</string> + <string name="cordn_pinned_title">Rögzített üzenetek</string> + <string name="cordn_reactions_title">Reakciók</string> + <string name="cordn_requests_accept">Hozzáadás</string> + <string name="cordn_requests_admin_only">Csak az adminisztrátor fogadhat el. Egy adminisztrátorok nélküli csoportban ez mindenki.</string> + <string name="cordn_requests_check">Kérelmek ellenőrzése</string> + <string name="cordn_requests_decline">Elvetés</string> + <string name="cordn_requests_failed">Nem sikerült beolvasni a kérelmeket.</string> + <string name="cordn_requests_none">Senki sem vár a csatlakozásra.</string> + <string name="cordn_requests_title">Csatlakozási kérelmek</string> + <string name="cordn_send">Küldés</string> + <string name="cordn_send_failed">Ez nem került elküldésre.</string> + <string name="cordn_send_no_session">Ennek a csoportnak a koordinátora nincs nyitva, így még semmit sem lehet küldeni.</string> + <string name="cordn_share_copied">Másolva</string> + <string name="cordn_share_copy">Hivatkozás másolása</string> + <string name="cordn_share_explainer">Bárki, aki rendelkezik ezzel a hivatkozással, kérheti a csatlakozást. Egy adminisztrátornak még mindig be kell engednie.</string> + <string name="cordn_share_title">Csoport megosztása</string> + <string name="cordn_voice_play">Hangfelvétel lejátszása</string> + <string name="cordn_voice_record">Hangfelvétel készítése</string> + <string name="cordn_voice_stop">Leállítás és küldés</string> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-pl-rPL/strings.xml b/commonsUI/src/commonMain/composeResources/values-pl-rPL/strings.xml index 1166642559..906a63b569 100644 --- a/commonsUI/src/commonMain/composeResources/values-pl-rPL/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-pl-rPL/strings.xml @@ -2909,6 +2909,7 @@ <string name="community_rules_violation_wot_gate_failed">Nie spełniasz wymogów dotyczących rankingu zaufania tej społeczności.</string> <string name="community_rules_violation_stale_rules">Najnowszy dokument zawierający zasady społeczności został odrzucony jako nieaktualny.</string> <string name="marmot_groups_title">Grupy Marmot</string> + <string name="cordn_groups_title">Grupy Cordn</string> <string name="marmot_create_group">Utwórz Grupę</string> <string name="marmot_create_group_title">Utwórz Grupę Marmot</string> <string name="marmot_create_group_footer">Zostanie utworzona nowa grupa MLS. Następnie możesz dodać członków.</string> @@ -5460,14 +5461,120 @@ <!-- cordn: groups delivered by an MCP coordinator rather than by relays. The exposure strings state what the coordinator operator learns; see spec/00.md §8 and quartz/plans/2026-09-17-cordn-interop.md. --> + <string name="cordn_badge_coordinator">Grupa koordynatora</string> + <string name="cordn_badge_coordinator_desc">Dostarczone przez koordynatora, nie przez transmitery</string> + <string name="cordn_exposure_title">Co ten koordynator może zobaczyć</string> + <string name="cordn_exposure_subtitle">Koordynator to serwer, który przekazuje wszystkie wiadomości w tej grupie w kolejności. Różni się to od działania transmiterów — warto o tym wiedzieć, zanim potraktujesz to, jak zwykły czat.</string> + <string name="cordn_exposure_dimension_content">Treść wiadomości</string> + <string name="cordn_exposure_dimension_membership">Kto jest w grupie</string> + <string name="cordn_exposure_dimension_messaging">Kto co wysyła</string> + <string name="cordn_exposure_level_none">Nieczytelne</string> + <string name="cordn_exposure_level_pseudonymous">Z użyciem klucza jednorazowego</string> + <string name="cordn_exposure_level_identified">Powiązane z Twoim prawdziwym kontem</string> + <string name="cordn_exposure_details">Szczegóły</string> + <string name="cordn_exposure_coordinator">Koordynator %1$s</string> + <string name="cordn_exposure_differs">Nie jest to taka sama forma interakcji jak w grupie Marmot, gdzie żadna z osób prowadzących nie dominuje całej rozmowy.</string> + <string name="cordn_note_membership">Łączy nazwy rzeczywistych kont po obu stronach, dzięki czemu koordynator wie, kto należy do tej grupy.</string> + <string name="cordn_note_linked">Pojedynczy klucz jednorazowego użytku służy do wysyłania i pobierania danych dla każdej z grup, które tu masz, dzięki czemu koordynator może stwierdzić, że wszystkie one należą do ciebie.</string> + <string name="cordn_note_history">Każdy operator przechowuje kompletną, uporządkowaną historię wszystkich grup, które obsługuje.</string> + <string name="cordn_note_publication">Opublikowany przez Ciebie pakiet kluczy to podpisany zapis potwierdzający, że to konto korzysta z usługi cordn, a koordynator może go przekazać dowolnej osobie.</string> + <string name="cordn_note_sizes">Komunikaty nie są uzupełniane, więc koordynator widzi, jak długi jest każdy z nich.</string> + <string name="cordn_note_encryption_bug">Ten klient nie szyfruje danych przesyłanych do koordynatora. To błąd — prosimy o zgłoszenie tego.</string> + <string name="cordn_health_unknown">Jeszcze się nie skontaktowano</string> + <string name="cordn_health_ok">Odpowiedź</string> + <string name="cordn_health_down">Brak odpowiedzi</string> + <string name="cordn_link_explainer">Wklej link „cordn1…” udostępniony przez inną osobę, aby sprawdzić, który koordynator prowadzi tę grupę oraz jakie informacje o tobie uzyskałby ten operator, gdybyś do niej dołączył.</string> + <string name="cordn_link_field">cordn1…</string> + <string name="cordn_link_inspect">Zbadaj</string> + <string name="cordn_link_paste">Wklej</string> + <string name="cordn_link_clear">Wyczyść</string> + <string name="cordn_link_invalid">To nie jest link do grupy Cordn: %1$s</string> + <string name="cordn_link_group_id">ID grupy</string> + <string name="cordn_link_coordinator">Koordynator</string> + <string name="cordn_link_relays">Osiągalne przez</string> + <string name="cordn_link_no_coordinator">W tym linku nie podano nazwiska koordynatora, więc nie można z niego skorzystać samodzielnie. Zapytaj osobę, która go przesłała, który koordynator prowadzi tę grupę.</string> + <string name="cordn_link_not_joinable">Sprawdzenie linku nie powoduje dołączenia do niczego, a samo kliknięcie go nie czyni cię członkiem.</string> <!-- cordn entries in the settings catalog and the Messages chat-type list. Here rather than in amethyst/res because both screens read them through Res. --> + <string name="cordn_link_title">Link do grupy Cordn</string> + <string name="cordn_link_search_keywords">cordn koordynator grupa link zaproszenie mls czat metadane prywatność ujawnienie</string> + <string name="cordn_coordinators_title">Koordynatorzy Cordn</string> + <string name="cordn_coordinators_search_keywords">cordn koordynator dodawanie usuwanie czyszczenie stan serwera mls czat sortowanie</string> + <string name="cordn_keypackages_title">Pakiety kluczy Cordn</string> + <string name="cordn_keypackages_search_keywords">cordn klucz pakiet publikacja zaproszenie ostatni resort koordynator mls</string> + <string name="cordn_backup_title">Kopia zapasowa Cordn</string> + <string name="cordn_backup_search_keywords">cordn kopia zapasowa przywracanie eksport import hasło odzyskiwanie grup</string> + <string name="chat_type_cordn_title">Grupy Cordn</string> + <string name="chat_type_cordn_desc">Grupy z szyfrowaniem typu end-to-end, zarządzane przez wybranego przez Ciebie koordynatora, a nie przez transmitery.</string> <!-- cordn strings on screens main migrated to Res: the shared inbox row and the new-conversation picker. --> + <string name="cordn_group_untitled">Grupa %1$s</string> + <string name="cordn_group_no_messages_yet">Brak wiadomości</string> + <string name="cordn_group_via_coordinator">Grupa Cordn, obsługiwana przez %1$s</string> + <string name="cordn_preview_deleted">Wiadomość usunięta</string> + <string name="cordn_preview_voice_note">Wiadomość głosowa</string> + <string name="cordn_preview_photo">Zdjęcie</string> + <string name="cordn_preview_video">Wideo</string> + <string name="cordn_preview_file">Plik</string> + <string name="new_conversation_cordn_title">Grupa Cordn</string> + <string name="new_conversation_cordn_tagline">Szyfrowany czat grupowy zorganizowany przez wybranego przez Ciebie koordynatora.</string> + <string name="new_conversation_cordn_chip">Skoordynowane</string> + <string name="new_conversation_cordn_best">Najlepsze rozwiązanie dla zespołów, które chcą mieć jeden spójny porządek dyskusji.</string> + <string name="new_conversation_cordn_cta">Utwórz grupę Cordn</string> + <string name="new_conversation_cordn_pro_1">Szyfrowanie typu „end-to-end” z wykorzystaniem MLS — koordynator nigdy nie ma możliwości odczytania wiadomości.</string> + <string name="new_conversation_cordn_pro_2">Jedno uzgodnione postanowienie dla wszystkich, aby historia nie mogła zostać zmieniona przez błędny zegar.</string> + <string name="new_conversation_cordn_con_1">Koordynator wie, kto należy do grupy i kiedy ktoś zabiera głos, mimo że nie potrafi odczytać treści wypowiedzi.</string> + <string name="cordn_invitations_entry">Zostałeś zaproszony do grupy „cordn”?</string> + <string name="cordn_invitations_entry_action">Przejrzyj zaproszenia</string> <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <string name="cordn_hub_title">Cordn</string> + <string name="cordn_hub_explainer">Grupy prowadzone przez wybranego przez Ciebie koordynatora, a nie przez transmitery.</string> + <string name="cordn_hub_link">Sprawdź link do grupy</string> + <string name="cordn_hub_link_desc">Zanim klikniesz w link typu „cordn1…”, sprawdź, dokąd prowadzi</string> + <string name="cordn_migrate_section_send">Zmień ten telefon</string> + <string name="cordn_migrate_section_receive">Przeniesienie danych z innego telefonu</string> + <string name="cordn_keypackages_section">Opublikowane pakiety kluczy</string> + <string name="cordn_link_section_input">Link</string> + <string name="cordn_coordinators_section_yours">Koordynatorzy, z których korzystasz</string> + <string name="cordn_coordinators_section_discover">Przedstawianie się</string> + <string name="cordn_coordinators_section_manual">Dodaj jedną ręcznie</string> + <string name="cordn_backup_section_passphrase">Hasło</string> + <string name="cordn_backup_section_export">Utwórz kopię zapasową</string> + <string name="cordn_backup_section_restore">Przywróć z kopii zapasowej</string> + <string name="cordn_hub_section_service">Koordynator</string> + <string name="cordn_hub_section_device">Urządzenie</string> + <string name="cordn_hub_coordinators">Koordynatorzy</string> + <string name="cordn_hub_coordinators_desc">Serwery z Twoimi grupami</string> + <string name="cordn_hub_keypackages">Pakiety kluczy</string> + <string name="cordn_hub_keypackages_desc">Co pozwala innym osobom dodać cię do grupy</string> + <string name="cordn_hub_backup">Kopia zapasowa i przywracanie</string> + <string name="cordn_hub_backup_desc">Przechowywany plik zaszyfrowany hasłem</string> + <string name="cordn_hub_migrate">Przenieś na nowy telefon</string> + <string name="cordn_hub_migrate_desc">Przekaż swoje grupy na urządzenie, na które się przełączasz</string> + <string name="cordn_migrate_title">Przenieś na nowy telefon</string> + <string name="cordn_migrate_explainer">Twoje grupy Cordn są przechowywane na tym urządzeniu. Przeniesienie ich wymaga jednego kodu: ten telefon je publikuje, nowy telefon je odczytuje, a ten telefon przestaje je wyświetlać.</string> + <string name="cordn_migrate_sign_in_first">Najpierw zaloguj się na nowym telefonie przy użyciu tego samego konta. Spowoduje to przeniesienie twoich grup, ale w żadnym wypadku nie twojego klucza.</string> + <string name="cordn_migrate_export">Rozpocznij transfer</string> + <string name="cordn_migrate_exporting">Publikowanie…</string> + <string name="cordn_migrate_scan_this">Zeskanuj to na nowym telefonie</string> + <string name="cordn_migrate_code_note">Kod to tylko wskazówka. Ktokolwiek go sfotografuje, nic się nie dowie — to, na co wskazuje, jest dla ciebie zastrzeżone.</string> + <string name="cordn_migrate_handed_off">To urządzenie przeniosło swoje grupy</string> + <string name="cordn_migrate_handed_off_desc">Aplikacja przestała wysyłać i odbierać wiadomości, ponieważ wysyłanie wiadomości przez dwa telefony z tej samej grupy spowodowałoby zakłócenia dla wszystkich jej członków. Twoje grupy nadal znajdują się na tym urządzeniu i nic nie zostało usunięte.</string> + <string name="cordn_migrate_cancel">Przywróć to urządzenie</string> + <string name="cordn_migrate_cancel_desc">Skorzystaj z tej opcji, jeśli przenoszenie nie zostało zakończone. Zrób to tylko wtedy, gdy nowy telefon jeszcze nie rozpoczął wysyłania danych.</string> + <string name="cordn_migrate_receive">Odbierz z innego telefonu</string> + <string name="cordn_migrate_scan">Zeskanuj kod</string> + <string name="cordn_migrate_paste">lub wklej</string> + <string name="cordn_migrate_import">Przynieś grupy tutaj</string> + <string name="cordn_migrate_importing">Pobieranie…</string> + <string name="cordn_migrate_replaces_warning">Powyższe dane zastępują wszelkie grupy „cordn” już istniejące w tym telefonie. Nie można ich połączyć.</string> + <string name="cordn_migrate_no_groups">Na tym urządzeniu nie ma żadnych grup cordn, które można by przenieść.</string> + <string name="cordn_migrate_no_server">Najpierw skonfiguruj serwer multimediów — zaszyfrowane dokumenty muszą gdzieś się znaleźć, dopóki drugi telefon je pobierze.</string> + <string name="cordn_migrate_exposure_title">Co opuszcza to urządzenie</string> + <string name="cordn_migrate_exposure_body">Stan Twojej grupy jest szyfrowany i przesyłany na serwer multimediów, aby drugi telefon mógł go pobrać. Serwer ten nie jest w stanie odczytać tych danych – widzi jedynie rozmiar i czas. Jeśli chcesz, aby nic nie pozostało, usuń te pliki po zakończeniu.</string> <string name="sno_object_title">Obiekt 3D</string> <string name="sno_object_details">%1$d wierzchołków · %2$d ścian</string> <string name="sno_object_unreadable">Nie udało się odczytać tego obiektu 3D (reguła %1$s)</string> @@ -5519,6 +5626,111 @@ <item quantity="other">%1$d bajta danych, których ten klient nie odczytuje</item> </plurals> <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> + <string name="browser_pill_close">Zamknij</string> + <string name="browser_pill_tile_copy">Kopiuj link</string> + <string name="browser_pill_tile_find">Znajdź</string> + <string name="browser_pill_tile_text">Rozmiar tekstu</string> + <string name="browser_pill_tile_desktop">Pulpit</string> + <string name="browser_pill_tile_home">Dodaj do strony głównej</string> + <string name="browser_pill_tile_other">Inna przeglądarka</string> + <string name="browser_pill_tile_full">Pełny ekran</string> + <string name="browser_pill_edit_address">Edytuj adres</string> + <string name="browser_pill_back">Wróć</string> + <string name="browser_pill_forward">Przekaż</string> + <string name="browser_pill_reload">Odśwież</string> + <string name="browser_pill_stop">Zatrzymaj wczytywanie</string> + <string name="browser_pill_favorite_add">Dodaj do ulubionych</string> + <string name="browser_pill_favorite_remove">Usuń z ulubionych</string> + <string name="browser_pill_share">Udostępnij</string> + <string name="browser_pill_copy">Kopiuj link</string> + <string name="browser_pill_find">Znajdź na stronie</string> + <string name="browser_pill_text_size">Rozmiar tekstu</string> + <string name="browser_pill_add_home">Dodaj do strony głównej</string> + <string name="browser_pill_desktop">Witryna stacjonarna</string> + <string name="browser_pill_other_browser">Otwórz w przeglądarce</string> + <string name="browser_pill_other_browser_named">Otwórz w %1$s</string> + <string name="browser_pill_full_screen">Pełny ekran</string> + <string name="browser_pill_left_site">Opuściłeś %1$s</string> + <string name="browser_pill_back_to_app">Powrót do aplikacji</string> + <string name="browser_pill_text_smaller">Mniejszy tekst</string> + <string name="browser_pill_text_larger">Większy tekst</string> + <string name="browser_pill_text_reset">Zresetuj</string> + <string name="browser_pill_text_value">%1$d%</string> + <string name="browser_pill_privacy">Prywatność</string> + <string name="browser_pill_tor_title">Przekierowanie Onion</string> + <string name="browser_pill_tor_on">Witryna nie widzi Twojego adresu IP</string> + <string name="browser_pill_tor_off">Witryna może wykryć Twój adres IP</string> + <string name="browser_pill_site_settings">Ustawienia witryny</string> + <string name="browser_pill_site_settings_none">Na razie nic nie jest dozwolone</string> + <string name="browser_pill_access">Do czego ma dostęp</string> + <string name="browser_pill_access_desc">Twoje klucze nigdy nie opuszczają Amethystu</string> + <string name="browser_pill_access_keys_desc">Każdy wpis, publikacja, przesłanie treści lub płatność wymaga Twojej zgody</string> + <string name="browser_pill_access_none">Brak specjalnego dostępu</string> + <string name="browser_pill_access_none_desc">Działa w środowisku izolowanym i nie ma dostępu do Twojego konta</string> + <string name="browser_pill_access_manage">Zarządzaj uprawnieniami</string> + <string name="browser_pill_access_nsite">nSite · w środowisku izolowanym</string> + <string name="browser_pill_access_napplet">nApplet · w środowisku izolowanym</string> + <string name="browser_pill_console">Konsola</string> + <string name="browser_pill_security_https">Bezpieczne połączenie</string> + <string name="browser_pill_security_http">Niezabezpieczone</string> + <string name="browser_pill_security_tor">Przekierowanie przez Onion</string> + <string name="browser_pill_security_sandbox">Aplikacja w środowisku izolowanym</string> + <string name="browser_pill_permission_camera">Aparat</string> + <string name="browser_pill_permission_microphone">Mikrofon</string> + <string name="browser_pill_permission_location">Lokalizacja</string> + <string name="browser_pill_decision_ask">Zapytaj</string> + <string name="browser_pill_decision_allow">Zezwól</string> + <string name="browser_pill_decision_block">Zablokuj</string> + <string name="browser_pill_permission_state">%1$s %2$s</string> + <string name="browser_pill_decision_allowed">dozwolone</string> + <string name="browser_pill_decision_blocked">zablokowane</string> + <string name="browser_pill_address_hint">Szukaj lub wprowadź adres</string> + <string name="browser_pill_go">Przejdź</string> + <string name="browser_pill_clear">Wyczyść</string> + <string name="browser_pill_paste_go">Wklej i przejdź</string> + <string name="browser_pill_fill_in">Użyj tego adresu</string> + <string name="browser_pill_find_hint">Znajdź na stronie</string> + <string name="browser_pill_find_count">%1$d x %2$d</string> + <string name="browser_pill_find_none">Brak wyników</string> + <string name="browser_pill_find_previous">Poprzedni wynik</string> + <string name="browser_pill_find_next">Następne wynik</string> + <string name="browser_pill_find_close">Zamknij wyszukiwanie na stronie</string> + <string name="browser_pill_console_all">Wszytko</string> + <string name="browser_pill_console_errors">Błędy</string> + <string name="browser_pill_console_warnings">Ostrzeżenia</string> + <string name="browser_pill_console_copy">Kopiuj</string> + <string name="browser_pill_console_clear">Wyczyść</string> + <string name="browser_pill_console_empty">Nic nie zalogowano</string> + <string name="browser_pill_perm_title">Ta strona chce użyć</string> + <string name="browser_pill_perm_camera_desc">Zobacz, co widzi Twoja kamera</string> + <string name="browser_pill_perm_microphone_desc">Usłysz, co słyszy Twój mikrofon</string> + <string name="browser_pill_perm_location_desc">Wiedz mniej więcej, gdzie się znajdujesz</string> + <string name="browser_pill_perm_tor_note">Połączenia mogą ujawnić Twój adres IP nawet przy korzystaniu z sieci Tor.</string> + <string name="browser_pill_perm_allow">Zezwól podczas odwiedzin</string> + <string name="browser_pill_perm_once">Tylko tym razem</string> + <string name="browser_pill_perm_deny">Nie zezwalaj</string> + <string name="browser_pill_dialog_generic">Na tej stronie napisano</string> + <string name="browser_pill_dialog_block">Nie pozwól tej stronie wyświetlać więcej dialogów</string> + <string name="browser_pill_dialog_answer">Twoja odpowiedź</string> + <string name="browser_pill_dialog_leave_title">Opuścić witrynę?</string> + <string name="browser_pill_dialog_leave_message">Wprowadzone zmiany mogą nie zostać zapisane.</string> + <string name="browser_pill_dialog_leave">Wyjdź</string> + <string name="browser_pill_ok">OK</string> + <string name="browser_pill_cancel">Anuluj</string> + <string name="browser_pill_info_connection">Połączenie</string> + <string name="browser_pill_info_https">Połączenie jest zaszyfrowane</string> + <string name="browser_pill_info_https_desc">Przesyłane przez Ciebie informacje nie mogą zostać odczytane podczas przesyłania.</string> + <string name="browser_pill_info_http">Połączenie nie jest zaszyfrowane</string> + <string name="browser_pill_info_http_desc">Nie wpisuj haseł ani danych płatności.</string> + <string name="browser_pill_info_tor">Przekierowanie typu Onion przez sieć Tor</string> + <string name="browser_pill_info_open">Otwarta sieć</string> + <string name="browser_pill_info_certificate">Certyfikat</string> + <string name="browser_pill_info_certificate_desc">Wydany dla %1$s przez %2$s · ważny do %3$s</string> + <string name="browser_pill_info_permissions">Uprawnienia</string> + <string name="browser_pill_info_site_data">Pliki cookie i dane witryny</string> + <string name="browser_pill_info_site_data_desc">Loginy i preferencje tej witryny zapisane dla tego konta</string> + <string name="browser_pill_info_clear">Wyczyść dane witryny</string> + <string name="browser_pill_info_clear_confirm">Wylogować się z tej witryny i usunąć dane?</string> <string name="backup_action_block_again">Zablokuj %1$s ponownie</string> <string name="backup_action_keep_count">Zachowaj %1$s</string> <string name="backup_action_rejoin">Ponów %1$s</string> @@ -5907,4 +6119,24 @@ <string name="backup_type_trust_providers_explainer">Serwisy, którym ufasz, a które zajmują się klasyfikowaniem i weryfikacją osób w Twoim imieniu.</string> <string name="backup_type_trusted_relays">lista zaufanych transmiterów</string> <string name="backup_type_trusted_relays_explainer">Zaufane transmitery, z którymi bez wahania nawiązujesz połączenie.</string> + <string name="cordn_action_delete">Usuń</string> + <string name="cordn_action_edit">Edytuj</string> + <string name="cordn_action_editing">Edycja wiadomości</string> + <string name="cordn_action_go_to_message">Przejdź do wiadomości</string> + <string name="cordn_action_pin">Przypnij</string> + <string name="cordn_action_reply">Odpowiedz</string> + <string name="cordn_action_unpin">Odepnij</string> + <string name="cordn_backup_restore_confirm_title">Czy zamienić grupy cordn tego urządzenia?</string> + <string name="cordn_backup_restore_title">Przywróć</string> + <string name="cordn_backup_restored">Przywrócono.</string> + <string name="cordn_chat_empty_description">Wszystkie wiadomości wysyłane tutaj są szyfrowane dla tej grupy. Koordynator przekazuje je dalej, nie mając możliwości ich odczytania.</string> + <string name="cordn_chat_empty_title">Brak wiadomości</string> + <string name="cordn_chat_unread_divider">Nowe wiadomości</string> + <string name="cordn_chat_yesterday">Wczoraj</string> + <string name="cordn_composer_hint">Wiadomość</string> + <string name="cordn_coordinators_add_action">Dodaj</string> + <string name="cordn_coordinators_add_failed">Nie można otworzyć tego koordynatora.</string> + <string name="cordn_coordinators_copy_key">Skopiuj npub koordynatora</string> + <string name="cordn_coordinators_discover_action">Szukaj koordynatorów</string> + <string name="cordn_coordinators_discover_add">Dodaj</string> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-pt-rBR/strings.xml b/commonsUI/src/commonMain/composeResources/values-pt-rBR/strings.xml index 33a24e3cbd..a469cfbf88 100644 --- a/commonsUI/src/commonMain/composeResources/values-pt-rBR/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-pt-rBR/strings.xml @@ -166,6 +166,152 @@ <string name="road_event_traffic_jam">Congestionamento</string> <string name="road_event_unknown">Evento na via</string> <!-- NIP-CC geocaching (kinds 37516 / 7516) --> + <string name="geocache_unnamed">Geocache</string> + <string name="geocache_type_traditional">Tradicional</string> + <string name="geocache_type_multi">Multi-cache</string> + <string name="geocache_type_mystery">Mistério</string> + <string name="geocache_size_micro">Micro</string> + <string name="geocache_size_small">Pequeno</string> + <string name="geocache_size_regular">Regular</string> + <string name="geocache_size_large">Grande</string> + <string name="geocache_size_other">Outro tamanho</string> + <string name="geocache_difficulty_short">D%1$d</string> + <string name="geocache_terrain_short">T%1$d</string> + <string name="geocache_first_to_find">Primeiro a encontrar</string> + <string name="geocache_is_art">Arte</string> + <string name="geocache_mission">Key Quest</string> + <string name="geocache_needs_verification">Encontros verificados</string> + <string name="geocache_claimed">Reivindicado</string> + <string name="geocache_archived">Arquivado</string> + <string name="geocache_hint_tap_to_reveal">Travou? Toque para revelar a dica</string> + <string name="geocache_hint_other_reading">Ilegível? Toque para ver a outra leitura</string> + <string name="geocache_nearby">Por perto</string> + <string name="geocache_found_it">Encontrei!</string> + <string name="geocache_verified_find">Encontro verificado</string> + <string name="geocache_invalid_proof">A prova não confere</string> + <string name="geocache_loading">Carregando cache…</string> + <string name="geocache_hidden_by">Escondido por %1$s</string> + <string name="geocache_navigate">Navegar</string> + <string name="geocache_copy_coordinates">Copiar coordenadas</string> + <string name="geocache_coordinates_copied">Coordenadas copiadas</string> + <string name="geocache_photos_section">Fotos</string> + <string name="geocache_mission_section">Missão Key Quest</string> + <string name="geocache_logs_section">Logs</string> + <string name="geocache_no_logs">Ninguém registrou este cache ainda. Seja o primeiro.</string> + <string name="geocache_i_found_it">Encontrei</string> + <string name="geocache_didnt_find_it">Não encontrei</string> + <string name="geocache_add_note">Adicionar uma nota</string> + <string name="geocache_needs_maintenance">Informar que precisa de manutenção</string> + <string name="geocache_add_to_hunt">Adicionar a uma caçada</string> + <string name="geocache_hunt_new">Nova caçada…</string> + <string name="geocache_hunt_none_yet">Você ainda não criou uma caçada.</string> + <string name="geocache_hunt_already_on">Já está nesta caçada</string> + <plurals name="geocache_caches_here"> + <item quantity="one">%1$d cache aqui</item> + <item quantity="other">%1$d caches aqui</item> + </plurals> + <string name="geocache_photo_remove">Remover esta foto</string> + <string name="geocache_archived_notice">Este cache foi arquivado. Ele não está mais em jogo.</string> + <string name="geocache_ftf_won_by">Primeiro a encontrar: %1$s</string> + <string name="geocache_ftf_available">O primeiro a encontrar ainda está em aberto</string> + <string name="geocache_owner_actions">Dono</string> + <string name="geocache_owner_edit">Editar este cache</string> + <string name="geocache_owner_archive">Arquivar este cache</string> + <string name="geocache_owner_archive_confirm">Arquivar. Quem procurar verá que ele está fora de jogo, e a listagem continua legível.</string> + <string name="geocache_owner_lock_ftf">Confirmar o primeiro a encontrar</string> + <string name="geocache_owner_lock_ftf_explain">Publica uma tag F indicando %1$s. Isso é permanente.</string> + <string name="geocache_owner_no_finders">Ainda não há encontros verificados, então não há ninguém para confirmar.</string> + <string name="geocache_owner_show_qr">Mostrar o código de verificação</string> + <string name="geocache_owner_qr_warning">Imprima isto e coloque dentro do cache. O Amethyst não o armazena — se você perdê-lo, quem encontrar o cache não poderá mais provar que esteve lá.</string> + <string name="geocache_log_type_dnf">Não encontrei</string> + <string name="geocache_log_type_note">Nota</string> + <string name="geocache_log_type_maintenance">Precisa de manutenção</string> + <string name="geocache_log_title">Registrar um encontro</string> + <string name="geocache_log_cache_label">Cache</string> + <string name="geocache_log_your_log">Seu log</string> + <string name="geocache_log_placeholder">Como foi?</string> + <string name="geocache_log_add_photo">Adicionar uma foto</string> + <string name="geocache_log_mission_answer">Sua resposta da Key Quest</string> + <string name="geocache_log_proof_section">Prova de presença</string> + <string name="geocache_log_scan_code">Escaneie o código dentro do cache</string> + <string name="geocache_log_scan_again">Escanear outro código</string> + <string name="geocache_log_proof_verified">Verificado — você esteve no cache</string> + <string name="geocache_log_proof_explain">Assinado uma única vez com o código da caixa. A chave nunca é salva e nunca sai desta tela.</string> + <string name="geocache_log_proof_bad_code">Isso não é um código de verificação.</string> + <string name="geocache_log_proof_wrong_cache">Esse código pertence a outro cache.</string> + <string name="geocache_log_proof_optional">Este cache não exige prova, então você pode simplesmente registrá-lo.</string> + <string name="geocache_log_no_location_attached">Sua própria localização não é anexada a este log.</string> + <string name="geocache_log_post">Publicar</string> + <string name="geocache_log_comment_title">Adicionar um log</string> + <string name="geocache_tab_nearby">Por perto</string> + <string name="geocache_tab_map">Mapa</string> + <string name="geocache_tab_hunts">Caçadas</string> + <string name="geocache_tab_finds">Encontrados</string> + <string name="geocache_tab_mine">Meus</string> + <string name="geocache_filter_unfound">Não encontrados</string> + <string name="geocache_filter_difficulty">D ≤ %1$d</string> + <string name="geocache_filter_terrain">T ≤ %1$d</string> + <string name="geocache_filter_has_prize">Tem prêmio</string> + <string name="geocache_filter_verified_only">Exige prova</string> + <string name="geocache_use_my_location">Usar minha localização</string> + <string name="geocache_sorted_by_distance">Ordenado por distância</string> + <string name="geocache_sorted_by_recent">Mais recentes primeiro</string> + <string name="geocache_hide_a_cache">Esconder um cache</string> + <string name="geocache_empty_nearby">Ainda não há caches aqui. Tente uma lista de seguidos mais ampla ou esconda o primeiro.</string> + <string name="geocache_empty_finds">Você ainda não registrou nenhum encontro.</string> + <string name="geocache_empty_mine">Você ainda não escondeu nenhum cache.</string> + <string name="geocache_empty_hunts">Nenhuma caçada ainda.</string> + <string name="geocache_stat_finds">Encontros</string> + <string name="geocache_stat_ftf">Primeiros a encontrar</string> + <string name="geocache_stat_verified">Verificados</string> + <string name="geocache_stat_hidden">Escondidos</string> + <string name="geocache_map_recenter">Centralizar em mim</string> + <string name="geocache_map_long_press">Toque e segure no mapa para esconder um cache ali</string> + <string name="geocache_map_open">Abrir</string> + <string name="geocache_new_section_where">Onde</string> + <string name="geocache_new_section_what">O quê</string> + <string name="geocache_new_section_extras">Extras</string> + <string name="geocache_new_section_proof">Prova</string> + <string name="geocache_new_section_relays">Relays</string> + <string name="geocache_new_location_warning">Isto publica uma localização pública com precisão de cerca de 5 metros. Qualquer pessoa no Nostr pode vê-la, e um evento publicado não pode ser retirado.</string> + <string name="geocache_new_pick_location">Escolha o local</string> + <string name="geocache_new_ladder">Publica %1$s, além de células mais amplas de até 3 caracteres para a busca por proximidade.</string> + <string name="geocache_new_name">Nome</string> + <string name="geocache_new_description">Descrição</string> + <string name="geocache_new_type">Tipo</string> + <string name="geocache_new_size">Tamanho</string> + <string name="geocache_new_difficulty">Dificuldade</string> + <string name="geocache_new_terrain">Terreno</string> + <string name="geocache_new_hint">Dica</string> + <string name="geocache_new_hint_preview">Quem procurar verá</string> + <string name="geocache_new_hint_scramble">Embaralhar a dica (ROT13)</string> + <string name="geocache_new_mission">Missão Key Quest</string> + <string name="geocache_new_modifiers">Modificadores</string> + <string name="geocache_new_require_proof">Exigir prova de presença</string> + <string name="geocache_new_proof_ready">Uma chave de verificação foi gerada para este cache.</string> + <string name="geocache_new_log_relays">Onde os encontros devem ser registrados</string> + <string name="geocache_new_publish">Publicar</string> + <string name="geocache_new_needs_name">Dê um nome ao cache.</string> + <string name="geocache_new_needs_location">Escolha onde o cache está escondido.</string> + <string name="geocache_hunt_progress">%1$d de %2$d encontrados</string> + <string name="geocache_hunt_start">Começar a caçada</string> + <plurals name="geocache_hunt_caches"> + <item quantity="one">%1$d cache</item> + <item quantity="other">%1$d caches</item> + </plurals> + <string name="geocache_hunt_title">Título</string> + <string name="geocache_hunt_description">Descrição</string> + <string name="geocache_hunt_banner">Imagem de capa</string> + <string name="geocache_hunt_pick_caches">Caches, em ordem</string> + <string name="geocache_hunt_add_cache">Adicionar um cache</string> + <string name="geocache_hunt_move_up">Mover para cima</string> + <string name="geocache_hunt_move_down">Mover para baixo</string> + <string name="geocache_hunt_remove">Remover</string> + <string name="geocache_hunt_theme">Tema da página</string> + <string name="geocache_hunt_map_style">Estilo do mapa</string> + <string name="geocache_hunt_needs_title">Dê um título à caçada.</string> + <string name="geocache_hunt_needs_caches">Adicione pelo menos um cache.</string> + <string name="geocache_publish_failed">Não foi possível publicar. Verifique sua conexão com os relays e tente novamente.</string> <string name="podcast_value_zap_split_hint">Os zaps para isto são divididos entre:</string> <string name="podcast_value_split_percent">%1$d%%</string> <string name="podcast_value_for_value">Valor-para-Valor</string> @@ -188,6 +334,51 @@ </plurals> <string name="profile_banner">Banner do perfil</string> <string name="point_to_the_qr_code">Aponte para o código QR</string> + <string name="qr_scanner_camera_rationale">O Amethyst precisa da câmera para ler códigos QR. Nada é gravado nem enviado — os quadros são decodificados no dispositivo e descartados.</string> + <string name="qr_scanner_grant_camera">Permitir câmera</string> + <string name="qr_scanner_camera_blocked">O acesso à câmera está desativado para o Amethyst. Você pode reativá-lo nas configurações do Android.</string> + <string name="qr_scanner_open_settings">Abrir configurações</string> + <string name="qr_scanner_unavailable">O leitor de QR não pôde ser iniciado neste dispositivo. Você ainda pode colar o código como texto.</string> + <string name="qr_scanner_torch_on">Acender a lanterna</string> + <string name="qr_scanner_torch_off">Apagar a lanterna</string> + <string name="qr_scanner_dark_hint">Está escuro — experimente a lanterna</string> + <string name="qr_scanner_scan_image">Ler de uma imagem</string> + <string name="qr_scanner_paste">Colar</string> + <string name="qr_scanner_no_code_in_image">Nenhum código QR encontrado nessa imagem</string> + <string name="qr_scanner_clipboard_empty">Nada na área de transferência para ler</string> + <string name="qr_scanner_pick_one">Toque no código que você quer</string> + <plurals name="qr_scanner_sequence_progress"> + <item quantity="one">%1$s de %2$s parte capturada — continue</item> + <item quantity="other">%1$s de %2$s partes capturadas — continue</item> + </plurals> + <string name="qr_scanner_zoom_reset">Redefinir zoom</string> + <string name="qr_scanner_unsupported_title">O Amethyst não consegue abrir este código</string> + <string name="qr_scanner_unsupported_secret">Este código contém uma chave privada ou um segredo de pareamento, por isso o conteúdo não é exibido aqui. Use a tela que espera por ele.</string> + <string name="qr_scanner_kind_nsec">Isto é uma chave privada. Cole-a na tela de login.</string> + <string name="qr_scanner_kind_wallet">Isto é uma conexão de carteira. Adicione-a nas configurações da Carteira.</string> + <string name="qr_scanner_kind_bunker">Isto é um endereço de assinador remoto. O Amethyst fornece esses endereços para que outros apps possam pedir que ele assine — não é possível entrar com um deles.</string> + <string name="qr_scanner_kind_signer">Isto é um app pedindo para se conectar ao seu assinador. Abra-o pela tela do assinador NIP-46.</string> + <string name="qr_scanner_kind_lightning">Isto é uma fatura Lightning.</string> + <string name="qr_scanner_kind_cashu">Isto é um token Cashu.</string> + <string name="qr_scanner_kind_web">Isto é um link da web.</string> + <string name="qr_scanner_kind_text">Isto é texto simples, não um código Nostr.</string> + <string name="qr_scanner_kind_nostr_unsupported">Isto é um código Nostr, mas não um que esta tela consiga abrir.</string> + <string name="qr_scanner_label_profile">Perfil</string> + <string name="qr_scanner_label_note">Nota</string> + <string name="qr_scanner_label_nostr">Código Nostr</string> + <string name="qr_scanner_label_relay">Relay</string> + <string name="qr_scanner_label_nsec">Chave privada</string> + <string name="qr_scanner_label_wallet">Conexão de carteira</string> + <string name="qr_scanner_label_bunker">Endereço de assinador remoto</string> + <string name="qr_scanner_label_signer">Pedido de conexão ao assinador</string> + <string name="qr_scanner_label_lightning">Fatura Lightning</string> + <string name="qr_scanner_label_cashu">Token Cashu</string> + <string name="qr_scanner_label_web">Link da web</string> + <string name="qr_scanner_label_text">Texto simples</string> + <string name="qr_scanner_open_link">Abrir link</string> + <string name="qr_scanner_copy">Copiar</string> + <string name="qr_scanner_copied">Copiado</string> + <string name="qr_scanner_try_again">Ler novamente</string> <string name="show_qr">Mostrar QR</string> <string name="profile_image">Imagem de perfil</string> <string name="your_profile_image">Sua Foto de Perfil</string> @@ -638,17 +829,70 @@ <string name="workout_hours">Horas</string> <string name="workout_minutes">Minutos</string> <string name="workout_seconds">Segundos</string> + <string name="workout_suggestion_connect_title">Use os treinos que você já registrou</string> + <string name="workout_suggestion_connect_message">Conecte o Health Connect (Samsung Health, Google Fit, Fitbit, Garmin…) para montar a Minha Forma Física, seu próprio resumo de treinos, e para preencher este treino a partir de um que seu relógio já registrou, em vez de digitar cada número à mão.</string> <string name="workout_suggestion_connect_button">Conectar</string> <string name="workout_suggestion_distance_km">%1$s km</string> <string name="workout_from_health_connect">Do Health Connect</string> <string name="workout_suggestion_connect_details">O que o Amethyst lê</string> <string name="health_connect_rationale_title">Health Connect e Amethyst</string> + <string name="health_connect_rationale_headline">O Amethyst usa o Health Connect para montar a Minha Forma Física — seu próprio resumo de treinos, no seu celular.</string> + <string name="health_connect_rationale_intro">A Minha Forma Física (menu → Você → Minha Forma Física) mostra como você tem treinado: quanto fez nesta semana em comparação com a anterior, como seu tempo se divide entre corrida, ciclismo, caminhada, natação e academia, seus melhores desempenhos, quantos dias você treinou e sua sequência atual. O Amethyst monta esse panorama a partir dos treinos que seu relógio ou app de fitness já salvou no Health Connect, junto com os treinos que você registrou no próprio Amethyst. Tudo é calculado no seu celular e mostrado a você.</string> <string name="health_connect_rationale_what_title">O que o Amethyst lê e por quê</string> + <string name="health_connect_rationale_exercise">Exercício · qual atividade você fez, quando começou e quanto durou. É o treino em si: define sua contagem de treinos, seu tempo de treino, a divisão por atividade, seus dias ativos e sua sequência.</string> + <string name="health_connect_rationale_distance">Distância · até onde você foi, mostrada como sua distância nesta semana em comparação com a anterior, sua média semanal, sua distância por atividade e sua maior distância.</string> + <string name="health_connect_rationale_calories">Calorias ativas e totais · a energia gasta em um treino, mostrada como sua energia nesta semana e como ela se compara com a semana anterior. As calorias ativas são usadas quando sua fonte as registra; as calorias totais são a alternativa para fontes que só registram a energia total.</string> + <string name="health_connect_rationale_heart_rate">Frequência cardíaca · a média e o máximo de cada treino, combinados na sua frequência cardíaca média e mais alta do período. É o que diferencia uma semana leve de uma pesada com a mesma distância.</string> + <string name="health_connect_rationale_steps">Passos · a contagem de passos de uma corrida, caminhada ou trilha, mostrada como sua média semanal de passos e sua maior contagem de passos.</string> + <string name="health_connect_rationale_elevation">Ganho de elevação · quanto você subiu, mostrado como sua média semanal de subida e sua maior subida. É o que separa uma semana plana de uma com morros.</string> <string name="health_connect_rationale_limits_title">O que o Amethyst não faz</string> + <string name="health_connect_rationale_limit_window">Só lê treinos das últimas 4 semanas, e só enquanto a Minha Forma Física ou o editor de treinos estiver aberto. Nunca lê em segundo plano.</string> <string name="health_connect_rationale_limit_write">Nunca grava nada no Health Connect e nunca solicita seu trajeto de exercício, localização ou qualquer outro tipo de dado de saúde.</string> + <string name="health_connect_rationale_limit_publish">Nunca envia seus dados de saúde para lugar nenhum. A Minha Forma Física é calculada no seu celular e fica nele. Se depois você quiser publicar um treino, toque em Compartilhar nesse treino, leia a publicação que o Amethyst preencheu e publique você mesmo — um treino por vez, nunca automaticamente.</string> + <string name="health_connect_rationale_limit_optional">O recurso inteiro é opcional. Revogue as permissões no Health Connect a qualquer momento — a Minha Forma Física volta a exibir o convite para conectar — ou desative as sugestões do editor em Configurações → Configurações de composição. O resto do Amethyst continua funcionando.</string> <string name="health_connect_rationale_privacy_policy">Ler a política de privacidade completa</string> + <string name="my_fitness_title">Minha Forma Física</string> + <string name="my_fitness_window">Últimas 4 semanas</string> + <string name="my_fitness_loading_metrics">Carregando distância, calorias e frequência cardíaca…</string> + <string name="my_fitness_window_note">As últimas quatro semanas completas do seu treino: os treinos que você publicou, mais tudo o que o Health Connect registra neste celular. O Amethyst calcula o resumo no dispositivo e não publica nada dele.</string> + <string name="my_fitness_this_week">Esta semana</string> + <string name="my_fitness_vs_last_week">vs. semana passada</string> + <string name="my_fitness_weekly_average">Média semanal</string> + <string name="my_fitness_workouts">Treinos</string> + <string name="my_fitness_active_days">Dias ativos</string> + <string name="my_fitness_streak">Dias seguidos</string> + <string name="my_fitness_time">Tempo</string> <!-- Workout duration totals. %1$d is hours, %2$d is minutes; reorder and re-abbreviate freely — these are English abbreviations for hour/minute/second. --> + <string name="my_fitness_duration_hours_minutes">%1$d h %2$d min</string> + <string name="my_fitness_duration_minutes">%1$d min</string> + <string name="my_fitness_duration_seconds">%1$d s</string> + <string name="my_fitness_distance">Distância</string> + <string name="my_fitness_calories">Calorias</string> + <string name="my_fitness_steps">Passos</string> + <string name="my_fitness_elevation">Subida</string> + <string name="my_fitness_avg_heart_rate">FC média</string> + <string name="my_fitness_max_heart_rate">FC máx.</string> + <string name="my_fitness_by_activity">Por atividade</string> + <string name="my_fitness_bests">Melhores desempenhos</string> + <string name="my_fitness_best_longest_distance">Maior distância</string> + <string name="my_fitness_best_longest_duration">Treino mais longo</string> + <string name="my_fitness_best_biggest_climb">Maior subida</string> + <string name="my_fitness_best_most_steps">Mais passos</string> + <string name="my_fitness_best_highest_heart_rate">Maior frequência cardíaca</string> + <string name="my_fitness_recent">Treinos recentes</string> + <string name="my_fitness_empty">Nada registrado nas últimas 4 semanas. Os treinos que você publicar, e os que seu relógio registrar, aparecem aqui.</string> + <string name="my_fitness_connect_title">Acompanhe seu treino</string> + <string name="my_fitness_connect_message">Conecte o Health Connect para ver seus totais semanais, a tendência semana a semana, seus melhores desempenhos e sua sequência de treinos. Tudo fica no seu celular.</string> + <string name="my_fitness_connect_banner">Conecte o Health Connect para adicionar os treinos que seu relógio registra, com frequência cardíaca, passos e subida.</string> + <string name="my_fitness_connect_button">Conectar</string> + <string name="my_fitness_share">Compartilhar este treino</string> + <string name="my_fitness_unit_bpm">bpm</string> + <string name="my_fitness_unit_kcal">kcal</string> + <string name="my_fitness_unit_km">km</string> + <string name="my_fitness_unit_mi">mi</string> + <string name="my_fitness_unit_m">m</string> + <string name="my_fitness_unit_ft">pés</string> <string name="nsite_none_found">Nenhum nSite encontrado ainda.</string> <string name="browser_reload">Recarregar</string> <string name="browser_unsupported">O navegador integrado requer Android 11 ou superior.</string> @@ -1084,6 +1328,8 @@ <string name="poll_closing_date_time">Data e hora de encerramento da enquete</string> <string name="poll_closing_in">Enquete encerra em %1$s</string> <string name="looking_for_event">Procurando o evento %1$s</string> + <string name="looking_for_event_title">Carregando</string> + <string name="looking_for_private_event">Aguardando suas mensagens privadas carregarem…</string> <string name="send_zap">Enviar Zap</string> <string name="custom_zaps_add_a_message">Adicionar uma mensagem pública</string> <string name="custom_zaps_add_a_message_private">Adicionar uma mensagem privada</string> @@ -1223,6 +1469,8 @@ <!-- Elapsed call time. %1$d is minutes and %2$d seconds in the short form; in the long form %1$d is hours, %2$d minutes and %3$d seconds. Separator and field order are the translator's to change. --> + <string name="call_duration_minutes_seconds">%1$02d:%2$02d</string> + <string name="call_duration_hours_minutes_seconds">%1$d:%2$02d:%3$02d</string> <string name="call_dismiss">Dispensar</string> <string name="call_switch_camera">Trocar câmera</string> <string name="call_voice">Chamada de voz</string> @@ -1357,6 +1605,14 @@ <string name="user_preferences">Preferências do Usuário</string> <string name="settings_search_placeholder">Pesquisar configurações</string> <string name="settings_search_no_results">Nenhuma configuração encontrada para "%1$s"</string> + <string name="marmot_preview_group_updated">Grupo atualizado</string> + <string name="marmot_preview_media">Anexo</string> + <string name="marmot_preview_no_text">Mensagem</string> + <string name="marmot_preview_with_sender">%1$s: %2$s</string> + <string name="marmot_empty_create_action">Criar um grupo</string> + <string name="marmot_invite_device_banner">Os convites do Marmot estão indo para outro dispositivo e não podem ser abertos aqui.</string> + <string name="marmot_invite_device_banner_dismiss">Dispensar</string> + <string name="marmot_invite_device_publish">Publicar a partir deste dispositivo</string> <string name="reset_marmot_confirm_title">Redefinir estado do Marmot?</string> <string name="reset_marmot_confirm_body">Isto excluirá permanentemente todos os chats em grupo Marmot, histórico de mensagens e chaves MLS deste dispositivo para a conta atual. Os participantes não serão notificados e ainda poderão ver você nos grupos até a próxima confirmação deles. Esta ação não pode ser desfeita. Um novo KeyPackage será publicado na próxima sincronização do aplicativo.</string> <string name="reset_marmot_confirm_action">Redefinir</string> @@ -1627,6 +1883,7 @@ <string name="wallet_no_transactions_for_filter">Nenhuma transação corresponde a este filtro</string> <string name="wallet_loading">Carregando…</string> <string name="wallet_incoming">Recebido</string> + <string name="wallet_open_zapped_note">Abrir nota que recebeu o zap</string> <string name="wallet_outgoing">Enviado</string> <string name="wallet_refresh">Atualizar</string> <string name="wallet_filter_all">Todos</string> @@ -1943,6 +2200,8 @@ <string name="video_player_settings_action_pip_description">Reproduzir o vídeo em uma janela flutuante (oculto se não suportado)</string> <string name="video_player_settings_action_cast">Transmitir para dispositivo</string> <string name="video_player_settings_action_cast_description">Envia o vídeo para um receptor Chromecast na sua Wi-Fi (oculto para arquivos locais)</string> + <string name="video_player_settings_action_captions">Legendas</string> + <string name="video_player_settings_action_captions_description">Ativar ou desativar as legendas (oculto quando o vídeo não tem nenhuma)</string> <string name="audio_visualizer_settings_description">Escolha a animação exibida enquanto as notas de áudio são reproduzidas.</string> <string name="audio_visualizer_off">Desligado</string> <string name="audio_visualizer_bars">Barras de espectro</string> @@ -2630,6 +2889,7 @@ <string name="community_rules_violation_wot_gate_failed">Você não atende aos requisitos da rede de confiança desta comunidade.</string> <string name="community_rules_violation_stale_rules">O documento mais recente de regras da comunidade foi rejeitado por estar desatualizado.</string> <string name="marmot_groups_title">Grupos Marmot</string> + <string name="cordn_groups_title">Grupos Cordn</string> <string name="marmot_create_group">Criar grupo</string> <string name="marmot_create_group_title">Criar grupo Marmot</string> <string name="marmot_create_group_footer">Um novo grupo MLS será criado. Você pode adicionar membros depois.</string> @@ -2921,13 +3181,18 @@ <string name="app_notification_replies_channel_description">Notifica você quando alguém responde à sua publicação</string> <string name="app_notification_replies_channel_name">Respostas</string> <string name="app_notification_replies_summary">Novas respostas</string> + <string name="app_notification_reply_failed">Não enviada</string> <string name="app_notification_reply_label">Responder</string> + <string name="app_notification_reply_retry">Tentar novamente</string> + <string name="app_notification_reply_sending">Enviando…</string> + <string name="app_notification_reply_unconfirmed">Ainda enviando…</string> <string name="app_notification_reposts_channel_description">Notifica você quando alguém reposta sua publicação</string> <string name="app_notification_reposts_channel_message">%1$s repostou sua publicação</string> <string name="app_notification_reposts_channel_name">Repostagens</string> <string name="app_notification_reposts_summary">Novos reposts</string> <string name="app_notification_scheduled_posts_channel_description">Notificações quando um post agendado é publicado ou falha ao publicar.</string> <string name="app_notification_scheduled_posts_channel_name">Posts agendados</string> + <string name="app_notification_shortcut_account_removed">Esta conta foi removida do Amethyst.</string> <string name="app_notification_zaps_channel_description">Notifica quando alguém te manda um zap</string> <string name="app_notification_zaps_channel_message">%1$s sats</string> <string name="app_notification_zaps_channel_message_for">por %1$s</string> @@ -3128,6 +3393,10 @@ <string name="call_unmute">Ativar som</string> <string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration_with_user">URL de retorno de chamada não encontrado da resposta %1$s</string> <string name="cancel">Cancelar</string> + <string name="captions_off">Desativadas</string> + <string name="captions_turn_off">Desativar legendas</string> + <string name="captions_turn_on">Ativar legendas</string> + <string name="captions_unnamed_track">Legendas</string> <string name="cashu">Token Cashu</string> <string name="cashu_checking_mint">Verificando o mint…</string> <string name="cashu_failed_redemption">Não foi possível resgatar o Cashu</string> @@ -3605,10 +3874,12 @@ <string name="kind_adventure_prologue">Prólogo da aventura</string> <string name="kind_adventure_reading">Leitura da aventura</string> <string name="kind_adventure_scene">Cena da aventura</string> + <string name="kind_app_curation_set">Lista de apps</string> <string name="kind_app_recommendations">Recomendações de Apps</string> <string name="kind_appointment">Compromisso</string> <string name="kind_apps">Aplicativos</string> <string name="kind_appt_rsvp">RSVP do Compromisso</string> + <string name="kind_article_curation_set">Lista de artigos</string> <string name="kind_audio_header">Cabeçalho de Áudio</string> <string name="kind_audio_track">Faixa de Áudio</string> <string name="kind_authored_podcasts">Podcasts criados</string> @@ -3621,6 +3892,7 @@ <string name="kind_blossom_auth">Autenticação Blossom</string> <string name="kind_blossom_servers">Servidores Blossom</string> <string name="kind_bookmark_list">Lista de Favoritos</string> + <string name="kind_bookmark_set">Conjunto de favoritos</string> <string name="kind_broadcast_relays">Relays de Broadcast</string> <string name="kind_calendar">Calendário</string> <string name="kind_channel_definition">Definição de Canal</string> @@ -3658,6 +3930,7 @@ <string name="kind_emoji_packs">Pacotes de Emoji</string> <string name="kind_ephemeral_chat">Chat Efêmero</string> <string name="kind_ephemeral_chatrooms">Salas de Chat Efêmeras</string> + <string name="kind_favorite_algo_feeds">Feeds favoritos</string> <string name="kind_favorite_podcasts">Podcasts favoritos</string> <string name="kind_favorite_relays">Relays Favoritos</string> <string name="kind_file_headers">Cabeçalhos de Arquivo</string> @@ -3666,28 +3939,36 @@ <string name="kind_follow_packs">Pacotes de Seguir</string> <string name="kind_geohash_follows">Seguindo Geohash</string> <string name="kind_gift_wraps">Embrulhos de presente</string> + <string name="kind_git_authors">Autores Git</string> <string name="kind_git_issue">Issue do Git</string> <string name="kind_git_patch">Patch do Git</string> <string name="kind_git_pr">Pull request</string> <string name="kind_git_pr_update">Atualização de PR</string> <string name="kind_git_reply">Resposta do Git</string> <string name="kind_git_repo">Repositório Git</string> + <string name="kind_git_repositories">Repositórios Git</string> <string name="kind_git_status_applied">Status do Git: Aplicado</string> <string name="kind_git_status_closed">Status do Git: Fechado</string> <string name="kind_git_status_draft">Status do Git: Rascunho</string> <string name="kind_git_status_open">Status do Git: Aberto</string> + <string name="kind_good_wiki_authors">Autores de wiki confiáveis</string> + <string name="kind_good_wiki_relays">Relays de wiki confiáveis</string> <string name="kind_hashtag_follows">Hashtags seguidas</string> <string name="kind_highlights">Destaques</string> <string name="kind_http_auth">Autenticação HTTP</string> <string name="kind_index_relay_list">Lista de relays de índice</string> + <string name="kind_interest_set">Interesses</string> <string name="kind_live_chats">Chats ao vivo</string> <string name="kind_live_streams">Transmissões ao vivo</string> + <string name="kind_media_follows">Seguidos de mídia</string> + <string name="kind_media_starter_pack">Pacote inicial de mídia</string> <string name="kind_medical_data">Dados Médicos</string> <string name="kind_meeting_room">Sala de reunião</string> <string name="kind_meeting_space">Espaço de reunião</string> <string name="kind_music_playlist">Playlist de música</string> <string name="kind_music_track">Faixa de música</string> <string name="kind_mute_list">Lista de silenciados</string> + <string name="kind_mute_set_for_kind">Silenciado para o kind %1$s</string> <string name="kind_named_bookmarks">Favoritos nomeados</string> <string name="kind_nests_servers">Servidores Nests</string> <string name="kind_nip">CNPJ</string> @@ -3702,6 +3983,7 @@ <string name="kind_outbox_relays">Relays de Saída</string> <string name="kind_pay_to">Pagar para</string> <string name="kind_people_lists">Listas de pessoas</string> + <string name="kind_picture_curation_set">Lista de imagens</string> <string name="kind_pictures">Imagens</string> <string name="kind_pins">Fixados</string> <string name="kind_podcast_episode">Episódio de podcast</string> @@ -3728,6 +4010,7 @@ <string name="kind_search_relays">Relays de busca</string> <string name="kind_shorts">Shorts</string> <string name="kind_shorts_repl">Shorts (resposta)</string> + <string name="kind_simple_groups">Grupos</string> <string name="kind_torrent_comments">Comentários de torrent</string> <string name="kind_torrents">Torrents</string> <string name="kind_trusted_providers">Provedores confiáveis</string> @@ -3736,7 +4019,10 @@ <string name="kind_user_settings">Configurações do Usuário</string> <string name="kind_user_status">Status do usuário</string> <string name="kind_video">Vídeo</string> + <string name="kind_video_collaboration">Colaboração em vídeo</string> + <string name="kind_video_list">Lista de vídeos</string> <string name="kind_video_repl">Vídeo (resposta)</string> + <string name="kind_video_subtitles">Legendas de vídeo</string> <string name="kind_voice_msg">Mensagem de voz</string> <string name="kind_voice_reply">Resposta de voz</string> <string name="kind_wake">Despertar</string> @@ -3773,6 +4059,8 @@ <string name="location_change_place">Alterar lugar no mapa</string> <string name="location_channels">Canais de localização</string> <string name="location_pick_on_map">Escolher um lugar no mapa</string> + <string name="login_bunker_not_supported">O Amethyst para Android não consegue entrar com um endereço bunker://. Ele fornece esses endereços para que outros apps possam pedir que ele assine, mas não é possível entrar com um deles. Use um app assinador externo ou o Amethyst Desktop.</string> + <string name="login_nostrconnect_not_supported">Isso é um app pedindo para se conectar ao seu assinador, não uma forma de entrar. Entre primeiro e depois abra-o pela tela do assinador NIP-46.</string> <string name="login_with_a_private_key_to_be_able_to_boost_posts">Faça login com uma chave privada para poder impulsionar postagens</string> <string name="login_with_a_private_key_to_be_able_to_follow">Faça login com uma chave privada para poder seguir</string> <string name="login_with_a_private_key_to_be_able_to_hide_word">Você está usando uma chave pública e as chaves públicas são somente de leitura. Faça login com uma chave privada para poder ocultar uma palavra ou frase.</string> @@ -3863,6 +4151,7 @@ <string name="mute_notifications">Silenciar notificações</string> <string name="muted_button">Silenciado. Clique para ativar o som</string> <string name="my_blossom_data">Meus arquivos Blossom</string> + <string name="my_fitness">Minha Forma Física</string> <string name="my_lists">Minhas listas</string> <string name="name_is_required">Um nome é necessário</string> <string name="namecoin_response_time">%1$dms</string> @@ -4126,6 +4415,7 @@ </plurals> <string name="nip46_signer_remote_app">Aplicativo de assinador remoto</string> <string name="nip46_signer_title">Assinador Remoto</string> + <string name="nip51_list_has_private_members">Esta lista tem membros privados que só o autor pode ver</string> <plurals name="nip82_assets_count"> <item quantity="one">%1$d recurso incluído</item> <item quantity="other">%1$d recursos incluídos</item> @@ -4656,12 +4946,21 @@ <string name="route_calendars">Calendários</string> <string name="route_chess">Xadrez</string> <string name="route_discover">Descobrir</string> + <string name="route_edit_geocache">Editar cache</string> + <string name="route_edit_geocache_hunt">Editar caçada</string> + <string name="route_geocache_detail">Cache</string> + <string name="route_geocache_hunt_detail">Caçada</string> + <string name="route_geocache_hunts">Caçadas de caches</string> + <string name="route_geocaches">Geocaches</string> <string name="route_home">Início</string> <string name="route_import_follows">Importar Seguidos</string> + <string name="route_log_geocache_find">Registrar um achado</string> <string name="route_media">Mídia</string> <string name="route_messages">Mensagens</string> <string name="route_music_playlists">Listas de reprodução</string> <string name="route_music_tracks">Música</string> + <string name="route_new_geocache">Esconder um cache</string> + <string name="route_new_geocache_hunt">Nova caçada</string> <string name="route_notifications">Notificações</string> <string name="route_podcast_episodes">Episódios</string> <string name="route_podcasts">Podcasts</string> @@ -4851,6 +5150,18 @@ <string name="vanish_request_sent">Pedido para desaparecer enviado</string> <string name="video_codec_h265_description">Melhor qualidade em arquivos menores, mas nem todos os dispositivos suportam reprodução em H.265.</string> <string name="video_codec_h265_label">Usar codec H.265/HEVC</string> + <string name="video_collaboration_accepted_on">Aceitou um crédito de colaboração</string> + <string name="video_collaboration_declined_on">Recusou um crédito de colaboração</string> + <string name="video_credit_accepted">Crédito aceito</string> + <string name="video_credit_audio_from">Áudio de</string> + <string name="video_credit_featuring">Participação de</string> + <string name="video_credit_inspired_by">Inspirado em</string> + <string name="video_credit_references">Referências</string> + <string name="video_curation_set_private_items">Lista privada</string> + <plurals name="video_curation_set_video_count"> + <item quantity="one">%1$d vídeo</item> + <item quantity="other">%1$d vídeos</item> + </plurals> <string name="video_download_has_started_toast">O download do vídeo foi iniciado…</string> <string name="video_player_settings">Botões do reprodutor de vídeo</string> <string name="video_saved_to_the_gallery">Vídeo salvo na galeria de vídeos do telefone</string> @@ -4910,165 +5221,18 @@ <string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">O serviço Lightning do destinatário em %1$s não está disponível. Foi calculado a partir do endereço Lightning "%2$s". Erro: %3$s. Verifique se o servidor está funcionando e se o endereço Lightning está correto</string> <string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">Não foi possível criar uma fatura lightning a partir de %1$s: Elemento pr não encontrado no JSON resultante.</string> <string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">Não foi possível criar uma fatura com raios. Mensagem do %1$s: %2$s</string> + <string name="marmot_invite_device_success">Este dispositivo agora recebe seus convites do Marmot.</string> + <string name="marmot_invite_device_failure">Falha ao publicar o KeyPackage: %1$s</string> + <string name="marmot_invite_device_rejected">Nenhum relay aceitou o KeyPackage. Verifique suas configurações de relays e tente novamente.</string> + <string name="chat_delivery_sending">Enviando…</string> + <string name="chat_delivery_failed">Não foi possível enviar. Toque para tentar novamente.</string> + <plurals name="marmot_unread_messages"> + <item quantity="one">%1$d nova mensagem</item> + <item quantity="other">%1$d novas mensagens</item> + </plurals> <!-- cordn: groups delivered by an MCP coordinator rather than by relays. The exposure strings state what the coordinator operator learns; see spec/00.md §8 and quartz/plans/2026-09-17-cordn-interop.md. --> - <!-- cordn entries in the settings catalog and the Messages chat-type - list. Here rather than in amethyst/res because both screens read - them through Res. --> - <!-- cordn strings on screens main migrated to Res: the shared inbox row - and the new-conversation picker. --> - <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a - one-shot handoff rather than continuous sync. The exposure strings say - what leaves the device; the fork strings say why the old phone stops. --> - <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> - <string name="geocache_unnamed">Geocache</string> - <string name="geocache_type_traditional">Tradicional</string> - <string name="geocache_type_multi">Multi-cache</string> - <string name="geocache_type_mystery">Mistério</string> - <string name="geocache_size_micro">Micro</string> - <string name="geocache_size_small">Pequeno</string> - <string name="geocache_size_regular">Regular</string> - <string name="geocache_size_large">Grande</string> - <string name="geocache_size_other">Outro tamanho</string> - <string name="geocache_difficulty_short">D%1$d</string> - <string name="geocache_terrain_short">T%1$d</string> - <string name="geocache_first_to_find">Primeiro a encontrar</string> - <string name="geocache_is_art">Arte</string> - <string name="geocache_mission">Key Quest</string> - <string name="geocache_needs_verification">Encontros verificados</string> - <string name="geocache_claimed">Reivindicado</string> - <string name="geocache_archived">Arquivado</string> - <string name="geocache_hint_tap_to_reveal">Travou? Toque para revelar a dica</string> - <string name="geocache_hint_other_reading">Ilegível? Toque para ver a outra leitura</string> - <string name="geocache_nearby">Por perto</string> - <string name="geocache_found_it">Encontrei!</string> - <string name="geocache_verified_find">Encontro verificado</string> - <string name="geocache_invalid_proof">A prova não confere</string> - <string name="geocache_loading">Carregando cache…</string> - <string name="geocache_hidden_by">Escondido por %1$s</string> - <string name="geocache_navigate">Navegar</string> - <string name="geocache_copy_coordinates">Copiar coordenadas</string> - <string name="geocache_coordinates_copied">Coordenadas copiadas</string> - <string name="geocache_photos_section">Fotos</string> - <string name="geocache_mission_section">Missão Key Quest</string> - <string name="geocache_logs_section">Logs</string> - <string name="geocache_no_logs">Ninguém registrou este cache ainda. Seja o primeiro.</string> - <string name="geocache_i_found_it">Encontrei</string> - <string name="geocache_didnt_find_it">Não encontrei</string> - <string name="geocache_add_note">Adicionar uma nota</string> - <string name="geocache_needs_maintenance">Informar que precisa de manutenção</string> - <string name="geocache_add_to_hunt">Adicionar a uma caçada</string> - <string name="geocache_hunt_new">Nova caçada…</string> - <string name="geocache_hunt_none_yet">Você ainda não criou uma caçada.</string> - <string name="geocache_hunt_already_on">Já está nesta caçada</string> - <plurals name="geocache_caches_here"> - <item quantity="one">%1$d cache aqui</item> - <item quantity="other">%1$d caches aqui</item> - </plurals> - <string name="geocache_photo_remove">Remover esta foto</string> - <string name="geocache_archived_notice">Este cache foi arquivado. Ele não está mais em jogo.</string> - <string name="geocache_ftf_won_by">Primeiro a encontrar: %1$s</string> - <string name="geocache_ftf_available">O primeiro a encontrar ainda está em aberto</string> - <string name="geocache_owner_actions">Dono</string> - <string name="geocache_owner_edit">Editar este cache</string> - <string name="geocache_owner_archive">Arquivar este cache</string> - <string name="geocache_owner_archive_confirm">Arquivar. Quem procurar verá que ele está fora de jogo, e a listagem continua legível.</string> - <string name="geocache_owner_lock_ftf">Confirmar o primeiro a encontrar</string> - <string name="geocache_owner_lock_ftf_explain">Publica uma tag F indicando %1$s. Isso é permanente.</string> - <string name="geocache_owner_no_finders">Ainda não há encontros verificados, então não há ninguém para confirmar.</string> - <string name="geocache_owner_show_qr">Mostrar o código de verificação</string> - <string name="geocache_owner_qr_warning">Imprima isto e coloque dentro do cache. O Amethyst não o armazena — se você perdê-lo, quem encontrar o cache não poderá mais provar que esteve lá.</string> - <string name="geocache_log_type_dnf">Não encontrei</string> - <string name="geocache_log_type_note">Nota</string> - <string name="geocache_log_type_maintenance">Precisa de manutenção</string> - <string name="geocache_log_title">Registrar um encontro</string> - <string name="geocache_log_cache_label">Cache</string> - <string name="geocache_log_your_log">Seu log</string> - <string name="geocache_log_placeholder">Como foi?</string> - <string name="geocache_log_add_photo">Adicionar uma foto</string> - <string name="geocache_log_mission_answer">Sua resposta da Key Quest</string> - <string name="geocache_log_proof_section">Prova de presença</string> - <string name="geocache_log_scan_code">Escaneie o código dentro do cache</string> - <string name="geocache_log_scan_again">Escanear outro código</string> - <string name="geocache_log_proof_verified">Verificado — você esteve no cache</string> - <string name="geocache_log_proof_explain">Assinado uma única vez com o código da caixa. A chave nunca é salva e nunca sai desta tela.</string> - <string name="geocache_log_proof_bad_code">Isso não é um código de verificação.</string> - <string name="geocache_log_proof_wrong_cache">Esse código pertence a outro cache.</string> - <string name="geocache_log_proof_optional">Este cache não exige prova, então você pode simplesmente registrá-lo.</string> - <string name="geocache_log_no_location_attached">Sua própria localização não é anexada a este log.</string> - <string name="geocache_log_post">Publicar</string> - <string name="geocache_log_comment_title">Adicionar um log</string> - <string name="geocache_tab_nearby">Por perto</string> - <string name="geocache_tab_map">Mapa</string> - <string name="geocache_tab_hunts">Caçadas</string> - <string name="geocache_tab_finds">Encontrados</string> - <string name="geocache_tab_mine">Meus</string> - <string name="geocache_filter_unfound">Não encontrados</string> - <string name="geocache_filter_difficulty">D ≤ %1$d</string> - <string name="geocache_filter_terrain">T ≤ %1$d</string> - <string name="geocache_filter_has_prize">Tem prêmio</string> - <string name="geocache_filter_verified_only">Exige prova</string> - <string name="geocache_use_my_location">Usar minha localização</string> - <string name="geocache_sorted_by_distance">Ordenado por distância</string> - <string name="geocache_sorted_by_recent">Mais recentes primeiro</string> - <string name="geocache_hide_a_cache">Esconder um cache</string> - <string name="geocache_empty_nearby">Ainda não há caches aqui. Tente uma lista de seguidos mais ampla ou esconda o primeiro.</string> - <string name="geocache_empty_finds">Você ainda não registrou nenhum encontro.</string> - <string name="geocache_empty_mine">Você ainda não escondeu nenhum cache.</string> - <string name="geocache_empty_hunts">Nenhuma caçada ainda.</string> - <string name="geocache_stat_finds">Encontros</string> - <string name="geocache_stat_ftf">Primeiros a encontrar</string> - <string name="geocache_stat_verified">Verificados</string> - <string name="geocache_stat_hidden">Escondidos</string> - <string name="geocache_map_recenter">Centralizar em mim</string> - <string name="geocache_map_long_press">Toque e segure no mapa para esconder um cache ali</string> - <string name="geocache_map_open">Abrir</string> - <string name="geocache_new_section_where">Onde</string> - <string name="geocache_new_section_what">O quê</string> - <string name="geocache_new_section_extras">Extras</string> - <string name="geocache_new_section_proof">Prova</string> - <string name="geocache_new_section_relays">Relays</string> - <string name="geocache_new_location_warning">Isto publica uma localização pública com precisão de cerca de 5 metros. Qualquer pessoa no Nostr pode vê-la, e um evento publicado não pode ser retirado.</string> - <string name="geocache_new_pick_location">Escolha o local</string> - <string name="geocache_new_ladder">Publica %1$s, além de células mais amplas de até 3 caracteres para a busca por proximidade.</string> - <string name="geocache_new_name">Nome</string> - <string name="geocache_new_description">Descrição</string> - <string name="geocache_new_type">Tipo</string> - <string name="geocache_new_size">Tamanho</string> - <string name="geocache_new_difficulty">Dificuldade</string> - <string name="geocache_new_terrain">Terreno</string> - <string name="geocache_new_hint">Dica</string> - <string name="geocache_new_hint_preview">Quem procurar verá</string> - <string name="geocache_new_hint_scramble">Embaralhar a dica (ROT13)</string> - <string name="geocache_new_mission">Missão Key Quest</string> - <string name="geocache_new_modifiers">Modificadores</string> - <string name="geocache_new_require_proof">Exigir prova de presença</string> - <string name="geocache_new_proof_ready">Uma chave de verificação foi gerada para este cache.</string> - <string name="geocache_new_log_relays">Onde os encontros devem ser registrados</string> - <string name="geocache_new_publish">Publicar</string> - <string name="geocache_new_needs_name">Dê um nome ao cache.</string> - <string name="geocache_new_needs_location">Escolha onde o cache está escondido.</string> - <string name="geocache_hunt_progress">%1$d de %2$d encontrados</string> - <string name="geocache_hunt_start">Começar a caçada</string> - <plurals name="geocache_hunt_caches"> - <item quantity="one">%1$d cache</item> - <item quantity="other">%1$d caches</item> - </plurals> - <string name="geocache_hunt_title">Título</string> - <string name="geocache_hunt_description">Descrição</string> - <string name="geocache_hunt_banner">Imagem de capa</string> - <string name="geocache_hunt_pick_caches">Caches, em ordem</string> - <string name="geocache_hunt_add_cache">Adicionar um cache</string> - <string name="geocache_hunt_move_up">Mover para cima</string> - <string name="geocache_hunt_move_down">Mover para baixo</string> - <string name="geocache_hunt_remove">Remover</string> - <string name="geocache_hunt_theme">Tema da página</string> - <string name="geocache_hunt_map_style">Estilo do mapa</string> - <string name="geocache_hunt_needs_title">Dê um título à caçada.</string> - <string name="geocache_hunt_needs_caches">Adicione pelo menos um cache.</string> - <string name="geocache_publish_failed">Não foi possível publicar. Verifique sua conexão com os relays e tente novamente.</string> - <string name="cordn_groups_title">Grupos Cordn</string> <string name="cordn_badge_coordinator">Grupo com coordenador</string> <string name="cordn_badge_coordinator_desc">Entregue por um coordenador, não por relays</string> <string name="cordn_exposure_title">O que este coordenador pode ver</string> @@ -5106,6 +5270,9 @@ <string name="cordn_link_relays">Acessível por</string> <string name="cordn_link_no_coordinator">Este link não indica nenhum coordenador, então não pode ser seguido sozinho. Pergunte a quem o enviou qual coordenador transporta o grupo.</string> <string name="cordn_link_not_joinable">Inspecionar um link não entra em nada, e ter um link não torna você membro.</string> + <!-- cordn entries in the settings catalog and the Messages chat-type + list. Here rather than in amethyst/res because both screens read + them through Res. --> <string name="cordn_link_title">Link de grupo Cordn</string> <string name="cordn_link_search_keywords">cordn coordenador grupo link convite mls chat metadados privacidade exposição</string> <string name="cordn_coordinators_title">Coordenadores Cordn</string> @@ -5114,6 +5281,10 @@ <string name="cordn_keypackages_search_keywords">cordn pacote de chaves publicar convite último recurso coordenador mls</string> <string name="cordn_backup_title">Backup do Cordn</string> <string name="cordn_backup_search_keywords">cordn backup restaurar exportar importar senha grupos recuperação</string> + <string name="chat_type_cordn_title">Grupos Cordn</string> + <string name="chat_type_cordn_desc">Grupos com criptografia de ponta a ponta, ordenados por um coordenador que você escolhe, em vez de por relays.</string> + <!-- cordn strings on screens main migrated to Res: the shared inbox row + and the new-conversation picker. --> <string name="cordn_group_untitled">Grupo %1$s</string> <string name="cordn_group_no_messages_yet">Nenhuma mensagem ainda</string> <string name="cordn_group_via_coordinator">Grupo Cordn, transportado por %1$s</string> @@ -5122,8 +5293,19 @@ <string name="cordn_preview_photo">Foto</string> <string name="cordn_preview_video">Vídeo</string> <string name="cordn_preview_file">Arquivo</string> + <string name="new_conversation_cordn_title">Grupo Cordn</string> + <string name="new_conversation_cordn_tagline">Chat em grupo criptografado, ordenado por um coordenador que você escolhe.</string> + <string name="new_conversation_cordn_chip">Coordenado</string> + <string name="new_conversation_cordn_best">Ideal para equipes que querem uma ordem confiável única para a conversa.</string> + <string name="new_conversation_cordn_cta">Criar grupo Cordn</string> + <string name="new_conversation_cordn_pro_1">Criptografia de ponta a ponta com MLS — o coordenador nunca consegue ler uma mensagem.</string> + <string name="new_conversation_cordn_pro_2">Uma única ordem acordada para todos, então o histórico não pode ser embaralhado por um relógio errado.</string> + <string name="new_conversation_cordn_con_1">O coordenador fica sabendo quem está no grupo e quando vocês conversam, mesmo sem conseguir ler o que é dito.</string> <string name="cordn_invitations_entry">Convidado para um grupo cordn?</string> <string name="cordn_invitations_entry_action">Ver convites</string> + <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a + one-shot handoff rather than continuous sync. The exposure strings say + what leaves the device; the fork strings say why the old phone stops. --> <string name="cordn_hub_title">Cordn</string> <string name="cordn_hub_explainer">Grupos entregues por um coordenador que você escolhe, em vez de por relays.</string> <string name="cordn_hub_link">Inspecionar um link de grupo</string> @@ -5173,453 +5355,6 @@ <string name="cordn_migrate_no_server">Defina primeiro um servidor de mídia — os documentos criptografados precisam de um lugar para ficar enquanto o outro celular os busca.</string> <string name="cordn_migrate_exposure_title">O que sai deste dispositivo</string> <string name="cordn_migrate_exposure_body">O estado dos seus grupos é criptografado e enviado ao seu servidor de mídia para que o outro celular possa buscá-lo. Ele é ilegível para esse servidor, que vê apenas um tamanho e um horário. Exclua os blobs depois se não quiser deixar nada para trás.</string> - <string name="cordn_action_delete">Excluir</string> - <string name="cordn_action_edit">Editar</string> - <string name="cordn_action_editing">Editando sua mensagem</string> - <string name="cordn_action_go_to_message">Ir para a mensagem</string> - <string name="cordn_action_pin">Fixar</string> - <string name="cordn_action_reply">Responder</string> - <string name="cordn_action_unpin">Desafixar</string> - <string name="cordn_admin_action_failed">Não foi possível fazer essa alteração.</string> - <string name="cordn_admin_coordinator_silent">O coordenador não respondeu, então nada foi alterado. Tente novamente em instantes.</string> - <string name="cordn_admin_no_key_package">%1$s não publicou um pacote de chaves neste coordenador, então ainda não pode ser adicionado. Peça que abra o cordn neste coordenador primeiro.</string> - <string name="cordn_admin_no_self_remove">Você não pode remover a si mesmo de um grupo cordn.</string> - <string name="cordn_admin_no_welcome">Não foi possível dar a %1$s um meio de entrar no grupo, então a adição foi cancelada.</string> - <string name="cordn_admin_not_a_member">%1$s não está neste grupo.</string> - <string name="cordn_admin_someone">Essa pessoa</string> - <string name="cordn_admin_wrong_key_package">Este coordenador forneceu um pacote de chaves que pertence a outra pessoa, então %1$s não foi adicionado. Nada foi alterado.</string> - <string name="cordn_backup_contents_body">As chaves de criptografia dos seus grupos, seus pacotes de chaves e todas as mensagens desses grupos. Qualquer pessoa que abrir o arquivo pode ler as conversas. A senha é a única coisa que o protege depois que ele sai do Amethyst.</string> - <string name="cordn_backup_contents_title">O que há no arquivo</string> - <string name="cordn_backup_explainer">Sua nsec não recupera grupos cordn. Um grupo cordn existe como estado de criptografia neste dispositivo e como um fluxo ordenado em um coordenador que não consegue lê-lo — perca o dispositivo sem um backup e o grupo se vai, incluindo tudo o que já foi dito nele.</string> - <string name="cordn_backup_export">Exportar</string> - <string name="cordn_backup_exported">Backup salvo.</string> - <string name="cordn_backup_failed">Não funcionou. Verifique a senha e o arquivo.</string> - <string name="cordn_backup_passphrase">Senha</string> - <string name="cordn_backup_restore">Restaurar</string> - <string name="cordn_backup_restore_body">Restaurar substitui os grupos cordn deste dispositivo pelos do arquivo. Serve para um celular que substituiu outro — não é uma forma de usar os mesmos grupos em dois dispositivos ao mesmo tempo.</string> - <string name="cordn_backup_restore_confirm_body">Todos os grupos cordn atualmente neste dispositivo serão removidos e substituídos pelos do arquivo.\n\nNão restaure em um segundo celular enquanto o primeiro ainda estiver em uso. Ambos teriam as mesmas chaves de grupo e, assim que os dois enviarem uma mensagem, o grupo quebra para todos — silenciosamente e sem volta.</string> - <string name="cordn_backup_restore_confirm_title">Substituir os grupos cordn deste dispositivo?</string> - <string name="cordn_backup_restore_title">Restaurar</string> - <string name="cordn_backup_restored">Restaurado.</string> - <string name="cordn_chat_empty_description">Tudo o que for enviado aqui é criptografado para este grupo. O coordenador repassa as mensagens sem conseguir lê-las.</string> - <string name="cordn_chat_empty_title">Nenhuma mensagem ainda</string> - <string name="cordn_chat_unread_divider">Novas mensagens</string> - <string name="cordn_chat_yesterday">Ontem</string> - <string name="cordn_composer_hint">Mensagem</string> - <string name="cordn_coordinators_add_action">Adicionar</string> - <string name="cordn_coordinators_add_failed">Não foi possível abrir esse coordenador.</string> - <string name="cordn_coordinators_copy_key">Copiar a npub do coordenador</string> - <string name="cordn_coordinators_discover_action">Procurar coordenadores</string> - <string name="cordn_coordinators_discover_add">Adicionar</string> - <string name="cordn_coordinators_discover_explainer">Lê anúncios que seus relays já transportam. Nenhum coordenador é contatado, então nenhum deles fica sabendo que você procurou.</string> - <string name="cordn_coordinators_discover_failed">Não foi possível ler os anúncios</string> - <string name="cordn_coordinators_discover_none">Ninguém está se anunciando nos seus relays.</string> - <string name="cordn_coordinators_discover_seen">Anunciado há %1$s</string> - <string name="cordn_coordinators_discover_seen_on">Último anúncio: %1$s</string> - <plurals name="cordn_coordinators_discover_unheard"> - <item quantity="one">Nada encontrado, e %1$d dos seus relays não respondeu.</item> - <item quantity="other">Nada encontrado, e %1$d dos seus relays não responderam.</item> - </plurals> - <string name="cordn_coordinators_explainer">Um coordenador ordena as mensagens de um grupo e é o único que pode fazer isso. Perdê-lo significa perder a ordenação, e um segundo coordenador não espelha o primeiro — por isso são escolhas nomeadas por você, não relays intercambiáveis.</string> - <plurals name="cordn_coordinators_health_down"> - <item quantity="one">Sem resposta — %1$d chamada seguida falhou. Os grupos nele não estão sincronizando.</item> - <item quantity="other">Sem resposta — %1$d chamadas seguidas falharam. Os grupos nele não estão sincronizando.</item> - </plurals> - <string name="cordn_coordinators_health_ok">Respondendo.</string> - <string name="cordn_coordinators_health_retrying">Uma chamada falhou; tentando novamente.</string> - <string name="cordn_coordinators_health_unknown">Nada foi solicitado a ele ainda.</string> - <string name="cordn_coordinators_hide_older">Ocultar os que pararam de se anunciar</string> - <string name="cordn_coordinators_identify">Perguntar quem é</string> - <string name="cordn_coordinators_key">Chave do coordenador</string> - <string name="cordn_coordinators_label">Um nome para ele (seu, opcional)</string> - <string name="cordn_coordinators_label_note">Este nome é seu e fica neste dispositivo. Um coordenador não pode comprovar um nome, então nunca é solicitado um.</string> - <string name="cordn_coordinators_more">Mais ações</string> - <string name="cordn_coordinators_none">Nenhum coordenador ainda.</string> - <string name="cordn_coordinators_purge">Expurgar</string> - <string name="cordn_coordinators_purge_body">Remover mantém os grupos neste dispositivo, então adicionar o coordenador de volta os traz junto. Expurgar os exclui: as chaves dos grupos, as posições de leitura e seus pacotes de chaves são apagados, e esses grupos só podem ser reabertos com um novo convite.\n\nO coordenador não é avisado e mantém tudo o que já tinha.</string> - <string name="cordn_coordinators_purge_title">Expurgar este coordenador?</string> - <string name="cordn_coordinators_relays">%1$s</string> - <string name="cordn_coordinators_relays_label">Responde em</string> - <string name="cordn_coordinators_relays_more">%1$s +%2$d mais</string> - <string name="cordn_coordinators_remove">Remover</string> - <string name="cordn_coordinators_rename">Renomear</string> - <string name="cordn_coordinators_server">Diz ser: %1$s</string> - <string name="cordn_coordinators_server_claim">O nome e a versão de um coordenador são textos escolhidos por ele. A chave pública é a única coisa que o identifica.</string> - <string name="cordn_coordinators_server_silent">Respondeu, mas não informou nenhum nome.</string> - <string name="cordn_coordinators_show_all">Mostrar todos os %1$d</string> - <string name="cordn_coordinators_show_fewer">Mostrar menos</string> - <plurals name="cordn_coordinators_show_older"> - <item quantity="one">Mostrar %1$d que parou de se anunciar</item> - <item quantity="other">Mostrar %1$d que pararam de se anunciar</item> - </plurals> - <string name="cordn_coordinators_stale_note">Estes não se anunciam há mais de um mês. Criar um grupo em um que tenha sumido vai falhar.</string> - <string name="cordn_create_action">Criar grupo</string> - <plurals name="cordn_create_action_invite"> - <item quantity="one">Criar e convidar %1$d pessoa</item> - <item quantity="other">Criar e convidar %1$d pessoas</item> - </plurals> - <string name="cordn_create_add_member">Adicionar alguém</string> - <string name="cordn_create_admin">Admin</string> - <plurals name="cordn_create_admin_count"> - <item quantity="one">%1$d pessoa pode adicionar e remover</item> - <item quantity="other">%1$d pessoas podem adicionar e remover</item> - </plurals> - <string name="cordn_create_admin_only_me">Só eu gerencio este grupo</string> - <string name="cordn_create_admin_only_me_on">Só você pode convidar, remover membros ou alterar os detalhes do grupo. Você pode passar isso para outra pessoa depois.</string> - <string name="cordn_create_coordinator">Coordenador</string> - <string name="cordn_create_coordinator_change">Alterar</string> - <string name="cordn_create_coordinator_discover">Encontrar coordenadores</string> - <string name="cordn_create_coordinator_hide">Concluído</string> - <string name="cordn_create_coordinator_new">Outro coordenador</string> - <string name="cordn_create_coordinator_pubkey">Chave pública do coordenador (hex ou npub)</string> - <string name="cordn_create_coordinator_relays">Relays em que ele responde, um por linha</string> - <plurals name="cordn_create_coverage_all"> - <item quantity="one">Alcança a %1$d pessoa escolhida</item> - <item quantity="other">Alcança todas as %1$d pessoas escolhidas</item> - </plurals> - <string name="cordn_create_coverage_partial">Alcança %1$d de %2$d — as demais ficariam de fora</string> - <string name="cordn_create_coverage_unknown">Não verificado — escolhê-lo fará a consulta</string> - <string name="cordn_create_description">Descrição (opcional)</string> - <string name="cordn_create_egalitarian">Todos podem adicionar e remover</string> - <string name="cordn_create_egalitarian_note">Todos no grupo podem convidar, remover membros e alterar os detalhes. Qualquer membro pode nomear admins depois, o que encerra esse modo.</string> - <string name="cordn_create_explainer">Um grupo cordn é ordenado por um coordenador que você escolhe. A criação do grupo acontece neste dispositivo — o coordenador só fica sabendo que o grupo existe quando você convida alguém ou envia uma mensagem.</string> - <string name="cordn_create_failed">Não foi possível criar o grupo.</string> - <string name="cordn_create_name">Nome do grupo</string> - <string name="cordn_create_no_coordinator">Nenhum coordenador escolhido</string> - <string name="cordn_create_outcome_explainer">Cada convite é um Welcome deixado no coordenador, então eles podem falhar um a um. Ninguém foi notificado: as pessoas verão o convite na próxima vez que abrirem o app.</string> - <string name="cordn_create_outcome_failed">O coordenador recusou o convite</string> - <string name="cordn_create_outcome_link">Enviar um link</string> - <string name="cordn_create_outcome_no_welcome">Adicionado ao grupo, mas o convite não chegou — tente novamente pela tela do grupo</string> - <string name="cordn_create_outcome_open">Abrir o chat</string> - <string name="cordn_create_outcome_skipped">Sem chave aqui, então nenhum Welcome pôde ser deixado</string> - <string name="cordn_create_outcome_title">O grupo foi criado</string> - <string name="cordn_create_outcome_waiting">Aguardando a pessoa</string> - <string name="cordn_create_people_done">Concluído</string> - <string name="cordn_create_people_empty">Ninguém adicionado ainda. Um grupo pode ser criado vazio e os convites enviados depois, mas indicar as pessoas agora é o que mostra qual coordenador consegue alcançá-las.</string> - <string name="cordn_create_people_none">Ninguém ainda — toque para adicionar pessoas</string> - <string name="cordn_create_people_note">Indicar as pessoas agora é o que nos diz qual coordenador consegue alcançá-las. Cada uma recebe um Welcome deixado nesse coordenador, que ela recebe na próxima vez que abrir o app.</string> - <plurals name="cordn_create_reach_count"> - <item quantity="one">Alcançável em %1$d coordenador</item> - <item quantity="other">Alcançável em %1$d coordenadores</item> - </plurals> - <string name="cordn_create_reach_none">Sem chave em nenhum coordenador que você usa</string> - <string name="cordn_create_reach_unknown">Ainda não verificado</string> - <string name="cordn_create_step_admins">Quem pode adicionar e remover</string> - <string name="cordn_create_step_name">Dê um nome</string> - <string name="cordn_create_step_people">Quem participa</string> - <string name="cordn_create_step_where">Onde ele fica</string> - <string name="cordn_create_title">Novo grupo Cordn</string> - <plurals name="cordn_create_unreachable_count"> - <item quantity="one">%1$d pessoa não pode ser alcançada por nenhum coordenador que você usa</item> - <item quantity="other">%1$d pessoas não podem ser alcançadas por nenhum coordenador que você usa</item> - </plurals> - <string name="cordn_create_you">Você, sempre admin</string> - <string name="cordn_delete_confirm_body">Todos no grupo verão esta mensagem como retirada, e o Amethyst deixará de mostrar o texto. O coordenador ainda guarda a cópia criptografada que foi enviada, e quem já a leu, já leu.</string> - <string name="cordn_delete_confirm_title">Retirar mensagem</string> - <string name="cordn_delivery_accepted">Aceita pelo coordenador</string> - <string name="cordn_exposure_close">Entendi</string> - <string name="cordn_exposure_open">O que este coordenador pode ver</string> - <string name="cordn_group_info">Informações do grupo</string> - <string name="cordn_group_unavailable">Este grupo não está disponível. O coordenador dele pode ter sido removido.</string> - <string name="cordn_group_unavailable_detail">Adicione um coordenador nas Configurações para começar a usar grupos cordn.</string> - <plurals name="cordn_groups_count"> - <item quantity="one">%1$d grupo</item> - <item quantity="other">%1$d grupos</item> - </plurals> - <string name="cordn_groups_down_note">Nada está chegando destes grupos. O Amethyst continua tentando.</string> - <string name="cordn_groups_manage">Coordenadores</string> - <string name="cordn_groups_none">Nenhum grupo cordn ainda</string> - <string name="cordn_groups_none_detail">Um grupo cordn fica em um coordenador que guarda a lista de membros e repassa as mensagens, sem nunca ver o que alguém escreve.</string> - <string name="cordn_groups_start">Criar um</string> - <string name="cordn_info_add_member">Adicionar alguém</string> - <string name="cordn_info_add_member_none">Ninguém encontrado com esse nome.</string> - <string name="cordn_info_add_member_note">Só é possível adicionar quem publicou um pacote de chaves neste coordenador.</string> - <string name="cordn_info_add_member_placeholder">Nome, npub ou nome@domínio</string> - <string name="cordn_info_admin_badge">Admin</string> - <plurals name="cordn_info_admins_kept"> - <item quantity="one">O %1$d admin do grupo continua como está.</item> - <item quantity="other">Os %1$d admins do grupo continuam como estão.</item> - </plurals> - <string name="cordn_info_coordinator">Coordenador</string> - <string name="cordn_info_coordinator_key">Chave do coordenador</string> - <string name="cordn_info_coordinator_nprofile">Link do coordenador</string> - <string name="cordn_info_copy_nprofile">Copiar o nprofile do coordenador</string> - <string name="cordn_info_edit_details">Editar detalhes</string> - <string name="cordn_info_egalitarian">Este grupo não tem admins: todos podem adicionar, remover e renomear, permanentemente.</string> - <string name="cordn_info_epoch">Época</string> - <string name="cordn_info_gid">ID do grupo</string> - <string name="cordn_info_has_key_package">Pode ser adicionado neste coordenador</string> - <string name="cordn_info_members">Membros</string> - <string name="cordn_info_remove_confirm_body">%1$s deixa de receber as mensagens daqui. A pessoa mantém o que já leu, e isso não pode ser desfeito — ela teria de ser convidada novamente.</string> - <string name="cordn_info_remove_confirm_title">Remover do grupo?</string> - <string name="cordn_info_remove_member">Remover</string> - <string name="cordn_info_save">Salvar</string> - <string name="cordn_info_technical">Detalhes técnicos</string> - <string name="cordn_invitations_accept">Entrar</string> - <string name="cordn_invitations_decline">Recusar</string> - <string name="cordn_invitations_decline_warning">Recusar é permanente. Para voltar, será preciso ser convidado novamente.</string> - <string name="cordn_invitations_explainer">Verificado quando você abre esta tela, e em nenhum outro momento — perguntar a um coordenador se alguém convidou você revela a ele que você está aqui, por isso o Amethyst não faz isso em segundo plano.</string> - <string name="cordn_invitations_load_failed">Não foi possível ler os convites.</string> - <plurals name="cordn_invitations_members"> - <item quantity="one">%1$d pessoa já está neste grupo</item> - <item quantity="other">%1$d pessoas já estão neste grupo</item> - </plurals> - <string name="cordn_invitations_members_more">+%1$d mais</string> - <string name="cordn_invitations_no_coordinators">Você não tem nenhum coordenador aberto. Os convites do cordn chegam por um coordenador, então ainda não há onde procurar.</string> - <string name="cordn_invitations_none">Nenhum convite pendente.</string> - <string name="cordn_invitations_refresh">Verificar novamente</string> - <string name="cordn_invitations_skipped">Um convite foi deixado para outro dispositivo</string> - <string name="cordn_invitations_title">Convites Cordn</string> - <string name="cordn_invitations_unreachable">%1$s não respondeu</string> - <string name="cordn_invitations_via">Por %1$s</string> - <string name="cordn_keypackages_disclosure">Publicar assina um registro com a chave da sua própria conta. Esse coordenador passa a saber que esta conta existe e pode ser convidada — permanentemente, e quer alguém convide você ou não.</string> - <string name="cordn_keypackages_explainer">Outras pessoas só podem adicionar você a um grupo cordn usando um pacote de chaves que você publicou nesse coordenador. O cordn não tem outro lugar para guardá-lo, então sem nenhum publicado ninguém pode convidar você — e nada diz a elas o porquê.</string> - <string name="cordn_keypackages_failed">Não foi possível ler os pacotes de chaves.</string> - <string name="cordn_keypackages_last_resort_no">Nenhum pacote de último recurso. Quando os de uso único acabarem, os convites falharão.</string> - <string name="cordn_keypackages_last_resort_yes">Um pacote de último recurso está publicado, então ainda é possível convidar você quando os de uso único acabarem.</string> - <plurals name="cordn_keypackages_orphans"> - <item quantity="one">%1$d pacote que este dispositivo não consegue abrir</item> - <item quantity="other">%1$d pacotes que este dispositivo não consegue abrir</item> - </plurals> - <string name="cordn_keypackages_orphans_body">O coordenador entregará estes pacotes a quem for convidar você, mas a metade privada não está neste dispositivo — ela pertence a outra instalação. Se essa instalação não existir mais, um convite que use um deles gera um Welcome que ninguém jamais conseguirá abrir.</string> - <string name="cordn_keypackages_publish">Publicar um</string> - <string name="cordn_keypackages_publish_last_resort">Publicar de último recurso</string> - <plurals name="cordn_keypackages_summary"> - <item quantity="one">%1$d pacote de uso único disponível.</item> - <item quantity="other">%1$d pacotes de uso único disponíveis.</item> - </plurals> - <string name="cordn_keypackages_topup_note">Depois que você publicar aqui, o Amethyst repõe discretamente os pacotes conforme forem usados. Ele nunca publica o primeiro por você.</string> - <string name="cordn_keypackages_withdraw_all">Retirar todos</string> - <string name="cordn_keypackages_withdraw_orphans">Retirar esses</string> - <string name="cordn_link_request">Pedir para entrar neste grupo</string> - <string name="cordn_link_request_disclosure">Isto publica um pacote de chaves com a chave da sua própria conta e informa ao coordenador que você quer entrar neste grupo — quer alguém responda ou não.</string> - <string name="cordn_link_request_failed">Não foi possível pedir para entrar.</string> - <string name="cordn_link_request_sent">Pedido enviado. Um membro do grupo precisa adicionar você; o convite aparecerá em Convites Cordn.</string> - <string name="cordn_link_scan">Escanear</string> - <string name="cordn_media_download_failed">Não foi possível abrir esse arquivo.</string> - <string name="cordn_media_no_server">Nenhum servidor de mídia está definido para esta conta, então não há onde colocar o arquivo. Escolha um nas Configurações.</string> - <string name="cordn_media_unreadable">Não foi possível ler esse arquivo.</string> - <string name="cordn_media_upload_failed">Não foi possível enviar esse arquivo.</string> - <plurals name="cordn_member_count"> - <item quantity="one">%1$d membro</item> - <item quantity="other">%1$d membros</item> - </plurals> - <string name="cordn_message_deleted">Mensagem excluída</string> - <string name="cordn_message_details_coordinator">Coordenador</string> - <string name="cordn_message_details_cursor">Cursor</string> - <string name="cordn_message_details_sent_at">Enviada</string> - <string name="cordn_message_details_title">Detalhes da mensagem</string> - <string name="cordn_message_edited">editada</string> - <string name="cordn_pinned_by">Fixada por %1$s</string> - <plurals name="cordn_pinned_count"> - <item quantity="one">%1$d mensagem fixada neste grupo</item> - <item quantity="other">%1$d mensagens fixadas neste grupo</item> - </plurals> - <string name="cordn_pinned_next">Próxima mensagem fixada</string> - <string name="cordn_pinned_previous">Mensagem fixada anterior</string> - <string name="cordn_pinned_show_all">Mostrar todas as mensagens fixadas</string> - <string name="cordn_pinned_title">Mensagens fixadas</string> - <string name="cordn_reactions_title">Reações</string> - <string name="cordn_requests_accept">Adicionar</string> - <string name="cordn_requests_admin_only">Só um admin pode aceitar. Em um grupo sem admins, isso vale para todos.</string> - <string name="cordn_requests_check">Verificar pedidos</string> - <string name="cordn_requests_decline">Dispensar</string> - <string name="cordn_requests_failed">Não foi possível ler os pedidos.</string> - <string name="cordn_requests_none">Ninguém está esperando para entrar.</string> - <string name="cordn_requests_title">Pedidos para entrar</string> - <string name="cordn_send">Enviar</string> - <string name="cordn_send_failed">Não foi enviada.</string> - <string name="cordn_send_no_session">O coordenador deste grupo não está aberto, então ainda não é possível enviar nada.</string> - <string name="cordn_share_copied">Copiado</string> - <string name="cordn_share_copy">Copiar link</string> - <string name="cordn_share_explainer">Qualquer pessoa com este link pode pedir para entrar. Um admin ainda precisa aceitá-la.</string> - <string name="cordn_share_title">Compartilhar este grupo</string> - <string name="cordn_voice_play">Reproduzir mensagem de voz</string> - <string name="cordn_voice_record">Gravar uma mensagem de voz</string> - <string name="cordn_voice_stop">Parar e enviar</string> - <string name="qr_scanner_camera_rationale">O Amethyst precisa da câmera para ler códigos QR. Nada é gravado nem enviado — os quadros são decodificados no dispositivo e descartados.</string> - <string name="qr_scanner_grant_camera">Permitir câmera</string> - <string name="qr_scanner_camera_blocked">O acesso à câmera está desativado para o Amethyst. Você pode reativá-lo nas configurações do Android.</string> - <string name="qr_scanner_open_settings">Abrir configurações</string> - <string name="qr_scanner_unavailable">O leitor de QR não pôde ser iniciado neste dispositivo. Você ainda pode colar o código como texto.</string> - <string name="qr_scanner_torch_on">Acender a lanterna</string> - <string name="qr_scanner_torch_off">Apagar a lanterna</string> - <string name="qr_scanner_dark_hint">Está escuro — experimente a lanterna</string> - <string name="qr_scanner_scan_image">Ler de uma imagem</string> - <string name="qr_scanner_paste">Colar</string> - <string name="qr_scanner_no_code_in_image">Nenhum código QR encontrado nessa imagem</string> - <string name="qr_scanner_clipboard_empty">Nada na área de transferência para ler</string> - <string name="qr_scanner_pick_one">Toque no código que você quer</string> - <plurals name="qr_scanner_sequence_progress"> - <item quantity="one">%1$s de %2$s parte capturada — continue</item> - <item quantity="other">%1$s de %2$s partes capturadas — continue</item> - </plurals> - <string name="qr_scanner_zoom_reset">Redefinir zoom</string> - <string name="qr_scanner_unsupported_title">O Amethyst não consegue abrir este código</string> - <string name="qr_scanner_unsupported_secret">Este código contém uma chave privada ou um segredo de pareamento, por isso o conteúdo não é exibido aqui. Use a tela que espera por ele.</string> - <string name="qr_scanner_kind_nsec">Isto é uma chave privada. Cole-a na tela de login.</string> - <string name="qr_scanner_kind_wallet">Isto é uma conexão de carteira. Adicione-a nas configurações da Carteira.</string> - <string name="qr_scanner_kind_bunker">Isto é um endereço de assinador remoto. O Amethyst fornece esses endereços para que outros apps possam pedir que ele assine — não é possível entrar com um deles.</string> - <string name="qr_scanner_kind_signer">Isto é um app pedindo para se conectar ao seu assinador. Abra-o pela tela do assinador NIP-46.</string> - <string name="qr_scanner_kind_lightning">Isto é uma fatura Lightning.</string> - <string name="qr_scanner_kind_cashu">Isto é um token Cashu.</string> - <string name="qr_scanner_kind_web">Isto é um link da web.</string> - <string name="qr_scanner_kind_text">Isto é texto simples, não um código Nostr.</string> - <string name="qr_scanner_kind_nostr_unsupported">Isto é um código Nostr, mas não um que esta tela consiga abrir.</string> - <string name="qr_scanner_label_profile">Perfil</string> - <string name="qr_scanner_label_note">Nota</string> - <string name="qr_scanner_label_nostr">Código Nostr</string> - <string name="qr_scanner_label_relay">Relay</string> - <string name="qr_scanner_label_nsec">Chave privada</string> - <string name="qr_scanner_label_wallet">Conexão de carteira</string> - <string name="qr_scanner_label_bunker">Endereço de assinador remoto</string> - <string name="qr_scanner_label_signer">Pedido de conexão ao assinador</string> - <string name="qr_scanner_label_lightning">Fatura Lightning</string> - <string name="qr_scanner_label_cashu">Token Cashu</string> - <string name="qr_scanner_label_web">Link da web</string> - <string name="qr_scanner_label_text">Texto simples</string> - <string name="qr_scanner_open_link">Abrir link</string> - <string name="qr_scanner_copy">Copiar</string> - <string name="qr_scanner_copied">Copiado</string> - <string name="qr_scanner_try_again">Ler novamente</string> - <string name="workout_suggestion_connect_title">Use os treinos que você já registrou</string> - <string name="workout_suggestion_connect_message">Conecte o Health Connect (Samsung Health, Google Fit, Fitbit, Garmin…) para montar a Minha Forma Física, seu próprio resumo de treinos, e para preencher este treino a partir de um que seu relógio já registrou, em vez de digitar cada número à mão.</string> - <string name="health_connect_rationale_headline">O Amethyst usa o Health Connect para montar a Minha Forma Física — seu próprio resumo de treinos, no seu celular.</string> - <string name="health_connect_rationale_intro">A Minha Forma Física (menu → Você → Minha Forma Física) mostra como você tem treinado: quanto fez nesta semana em comparação com a anterior, como seu tempo se divide entre corrida, ciclismo, caminhada, natação e academia, seus melhores desempenhos, quantos dias você treinou e sua sequência atual. O Amethyst monta esse panorama a partir dos treinos que seu relógio ou app de fitness já salvou no Health Connect, junto com os treinos que você registrou no próprio Amethyst. Tudo é calculado no seu celular e mostrado a você.</string> - <string name="health_connect_rationale_exercise">Exercício · qual atividade você fez, quando começou e quanto durou. É o treino em si: define sua contagem de treinos, seu tempo de treino, a divisão por atividade, seus dias ativos e sua sequência.</string> - <string name="health_connect_rationale_distance">Distância · até onde você foi, mostrada como sua distância nesta semana em comparação com a anterior, sua média semanal, sua distância por atividade e sua maior distância.</string> - <string name="health_connect_rationale_calories">Calorias ativas e totais · a energia gasta em um treino, mostrada como sua energia nesta semana e como ela se compara com a semana anterior. As calorias ativas são usadas quando sua fonte as registra; as calorias totais são a alternativa para fontes que só registram a energia total.</string> - <string name="health_connect_rationale_heart_rate">Frequência cardíaca · a média e o máximo de cada treino, combinados na sua frequência cardíaca média e mais alta do período. É o que diferencia uma semana leve de uma pesada com a mesma distância.</string> - <string name="health_connect_rationale_steps">Passos · a contagem de passos de uma corrida, caminhada ou trilha, mostrada como sua média semanal de passos e sua maior contagem de passos.</string> - <string name="health_connect_rationale_elevation">Ganho de elevação · quanto você subiu, mostrado como sua média semanal de subida e sua maior subida. É o que separa uma semana plana de uma com morros.</string> - <string name="health_connect_rationale_limit_window">Só lê treinos das últimas 4 semanas, e só enquanto a Minha Forma Física ou o editor de treinos estiver aberto. Nunca lê em segundo plano.</string> - <string name="health_connect_rationale_limit_publish">Nunca envia seus dados de saúde para lugar nenhum. A Minha Forma Física é calculada no seu celular e fica nele. Se depois você quiser publicar um treino, toque em Compartilhar nesse treino, leia a publicação que o Amethyst preencheu e publique você mesmo — um treino por vez, nunca automaticamente.</string> - <string name="health_connect_rationale_limit_optional">O recurso inteiro é opcional. Revogue as permissões no Health Connect a qualquer momento — a Minha Forma Física volta a exibir o convite para conectar — ou desative as sugestões do editor em Configurações → Configurações de composição. O resto do Amethyst continua funcionando.</string> - <string name="my_fitness_title">Minha Forma Física</string> - <string name="my_fitness_window">Últimas 4 semanas</string> - <string name="my_fitness_loading_metrics">Carregando distância, calorias e frequência cardíaca…</string> - <string name="my_fitness_window_note">As últimas quatro semanas completas do seu treino: os treinos que você publicou, mais tudo o que o Health Connect registra neste celular. O Amethyst calcula o resumo no dispositivo e não publica nada dele.</string> - <string name="my_fitness_this_week">Esta semana</string> - <string name="my_fitness_vs_last_week">vs. semana passada</string> - <string name="my_fitness_weekly_average">Média semanal</string> - <string name="my_fitness_workouts">Treinos</string> - <string name="my_fitness_active_days">Dias ativos</string> - <string name="my_fitness_streak">Dias seguidos</string> - <string name="my_fitness_time">Tempo</string> - <string name="my_fitness_duration_hours_minutes">%1$d h %2$d min</string> - <string name="my_fitness_duration_minutes">%1$d min</string> - <string name="my_fitness_duration_seconds">%1$d s</string> - <string name="my_fitness_distance">Distância</string> - <string name="my_fitness_calories">Calorias</string> - <string name="my_fitness_steps">Passos</string> - <string name="my_fitness_elevation">Subida</string> - <string name="my_fitness_avg_heart_rate">FC média</string> - <string name="my_fitness_max_heart_rate">FC máx.</string> - <string name="my_fitness_by_activity">Por atividade</string> - <string name="my_fitness_bests">Melhores desempenhos</string> - <string name="my_fitness_best_longest_distance">Maior distância</string> - <string name="my_fitness_best_longest_duration">Treino mais longo</string> - <string name="my_fitness_best_biggest_climb">Maior subida</string> - <string name="my_fitness_best_most_steps">Mais passos</string> - <string name="my_fitness_best_highest_heart_rate">Maior frequência cardíaca</string> - <string name="my_fitness_recent">Treinos recentes</string> - <string name="my_fitness_empty">Nada registrado nas últimas 4 semanas. Os treinos que você publicar, e os que seu relógio registrar, aparecem aqui.</string> - <string name="my_fitness_connect_title">Acompanhe seu treino</string> - <string name="my_fitness_connect_message">Conecte o Health Connect para ver seus totais semanais, a tendência semana a semana, seus melhores desempenhos e sua sequência de treinos. Tudo fica no seu celular.</string> - <string name="my_fitness_connect_banner">Conecte o Health Connect para adicionar os treinos que seu relógio registra, com frequência cardíaca, passos e subida.</string> - <string name="my_fitness_connect_button">Conectar</string> - <string name="my_fitness_share">Compartilhar este treino</string> - <string name="my_fitness_unit_bpm">bpm</string> - <string name="my_fitness_unit_kcal">kcal</string> - <string name="my_fitness_unit_km">km</string> - <string name="my_fitness_unit_mi">mi</string> - <string name="my_fitness_unit_m">m</string> - <string name="my_fitness_unit_ft">pés</string> - <string name="looking_for_event_title">Carregando</string> - <string name="looking_for_private_event">Aguardando suas mensagens privadas carregarem…</string> - <string name="call_duration_minutes_seconds">%1$02d:%2$02d</string> - <string name="call_duration_hours_minutes_seconds">%1$d:%2$02d:%3$02d</string> - <string name="marmot_preview_group_updated">Grupo atualizado</string> - <string name="marmot_preview_media">Anexo</string> - <string name="marmot_preview_no_text">Mensagem</string> - <string name="marmot_preview_with_sender">%1$s: %2$s</string> - <string name="marmot_empty_create_action">Criar um grupo</string> - <string name="marmot_invite_device_banner">Os convites do Marmot estão indo para outro dispositivo e não podem ser abertos aqui.</string> - <string name="marmot_invite_device_banner_dismiss">Dispensar</string> - <string name="marmot_invite_device_publish">Publicar a partir deste dispositivo</string> - <string name="wallet_open_zapped_note">Abrir nota que recebeu o zap</string> - <string name="video_player_settings_action_captions">Legendas</string> - <string name="video_player_settings_action_captions_description">Ativar ou desativar as legendas (oculto quando o vídeo não tem nenhuma)</string> - <string name="app_notification_reply_failed">Não enviada</string> - <string name="app_notification_reply_retry">Tentar novamente</string> - <string name="app_notification_reply_sending">Enviando…</string> - <string name="app_notification_reply_unconfirmed">Ainda enviando…</string> - <string name="app_notification_shortcut_account_removed">Esta conta foi removida do Amethyst.</string> - <string name="captions_off">Desativadas</string> - <string name="captions_turn_off">Desativar legendas</string> - <string name="captions_turn_on">Ativar legendas</string> - <string name="captions_unnamed_track">Legendas</string> - <string name="kind_app_curation_set">Lista de apps</string> - <string name="kind_article_curation_set">Lista de artigos</string> - <string name="kind_bookmark_set">Conjunto de favoritos</string> - <string name="kind_favorite_algo_feeds">Feeds favoritos</string> - <string name="kind_git_authors">Autores Git</string> - <string name="kind_git_repositories">Repositórios Git</string> - <string name="kind_good_wiki_authors">Autores de wiki confiáveis</string> - <string name="kind_good_wiki_relays">Relays de wiki confiáveis</string> - <string name="kind_interest_set">Interesses</string> - <string name="kind_media_follows">Seguidos de mídia</string> - <string name="kind_media_starter_pack">Pacote inicial de mídia</string> - <string name="kind_mute_set_for_kind">Silenciado para o kind %1$s</string> - <string name="kind_picture_curation_set">Lista de imagens</string> - <string name="kind_simple_groups">Grupos</string> - <string name="kind_video_collaboration">Colaboração em vídeo</string> - <string name="kind_video_list">Lista de vídeos</string> - <string name="kind_video_subtitles">Legendas de vídeo</string> - <string name="login_bunker_not_supported">O Amethyst para Android não consegue entrar com um endereço bunker://. Ele fornece esses endereços para que outros apps possam pedir que ele assine, mas não é possível entrar com um deles. Use um app assinador externo ou o Amethyst Desktop.</string> - <string name="login_nostrconnect_not_supported">Isso é um app pedindo para se conectar ao seu assinador, não uma forma de entrar. Entre primeiro e depois abra-o pela tela do assinador NIP-46.</string> - <string name="my_fitness">Minha Forma Física</string> - <string name="nip51_list_has_private_members">Esta lista tem membros privados que só o autor pode ver</string> - <string name="route_edit_geocache">Editar cache</string> - <string name="route_edit_geocache_hunt">Editar caçada</string> - <string name="route_geocache_detail">Cache</string> - <string name="route_geocache_hunt_detail">Caçada</string> - <string name="route_geocache_hunts">Caçadas de caches</string> - <string name="route_geocaches">Geocaches</string> - <string name="route_log_geocache_find">Registrar um achado</string> - <string name="route_new_geocache">Esconder um cache</string> - <string name="route_new_geocache_hunt">Nova caçada</string> - <string name="video_collaboration_accepted_on">Aceitou um crédito de colaboração</string> - <string name="video_collaboration_declined_on">Recusou um crédito de colaboração</string> - <string name="video_credit_accepted">Crédito aceito</string> - <string name="video_credit_audio_from">Áudio de</string> - <string name="video_credit_featuring">Participação de</string> - <string name="video_credit_inspired_by">Inspirado em</string> - <string name="video_credit_references">Referências</string> - <string name="video_curation_set_private_items">Lista privada</string> - <plurals name="video_curation_set_video_count"> - <item quantity="one">%1$d vídeo</item> - <item quantity="other">%1$d vídeos</item> - </plurals> - <string name="marmot_invite_device_success">Este dispositivo agora recebe seus convites do Marmot.</string> - <string name="marmot_invite_device_failure">Falha ao publicar o KeyPackage: %1$s</string> - <string name="marmot_invite_device_rejected">Nenhum relay aceitou o KeyPackage. Verifique suas configurações de relays e tente novamente.</string> - <string name="chat_delivery_sending">Enviando…</string> - <string name="chat_delivery_failed">Não foi possível enviar. Toque para tentar novamente.</string> - <plurals name="marmot_unread_messages"> - <item quantity="one">%1$d nova mensagem</item> - <item quantity="other">%1$d novas mensagens</item> - </plurals> - <string name="chat_type_cordn_title">Grupos Cordn</string> - <string name="chat_type_cordn_desc">Grupos com criptografia de ponta a ponta, ordenados por um coordenador que você escolhe, em vez de por relays.</string> - <string name="new_conversation_cordn_title">Grupo Cordn</string> - <string name="new_conversation_cordn_tagline">Chat em grupo criptografado, ordenado por um coordenador que você escolhe.</string> - <string name="new_conversation_cordn_chip">Coordenado</string> - <string name="new_conversation_cordn_best">Ideal para equipes que querem uma ordem confiável única para a conversa.</string> - <string name="new_conversation_cordn_cta">Criar grupo Cordn</string> - <string name="new_conversation_cordn_pro_1">Criptografia de ponta a ponta com MLS — o coordenador nunca consegue ler uma mensagem.</string> - <string name="new_conversation_cordn_pro_2">Uma única ordem acordada para todos, então o histórico não pode ser embaralhado por um relógio errado.</string> - <string name="new_conversation_cordn_con_1">O coordenador fica sabendo quem está no grupo e quando vocês conversam, mesmo sem conseguir ler o que é dito.</string> <string name="sno_object_title">Objeto 3D</string> <string name="sno_object_details">%1$d vértices · %2$d faces</string> <string name="sno_object_unreadable">Não foi possível ler este objeto 3D (regra %1$s)</string> @@ -5662,6 +5397,7 @@ <item quantity="one">%1$d byte de algo que este cliente não lê</item> <item quantity="other">%1$d bytes de algo que este cliente não lê</item> </plurals> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> <string name="browser_pill_close">Fechar</string> <string name="browser_pill_tile_copy">Copiar link</string> <string name="browser_pill_tile_find">Localizar</string> @@ -6092,4 +5828,268 @@ <string name="backup_type_trust_providers_explainer">Os serviços em que você confia para classificar e verificar pessoas para você.</string> <string name="backup_type_trusted_relays">lista de relays confiáveis</string> <string name="backup_type_trusted_relays_explainer">Relays em que você confia para se conectar sem perguntar.</string> + <string name="cordn_action_delete">Excluir</string> + <string name="cordn_action_edit">Editar</string> + <string name="cordn_action_editing">Editando sua mensagem</string> + <string name="cordn_action_go_to_message">Ir para a mensagem</string> + <string name="cordn_action_pin">Fixar</string> + <string name="cordn_action_reply">Responder</string> + <string name="cordn_action_unpin">Desafixar</string> + <string name="cordn_admin_action_failed">Não foi possível fazer essa alteração.</string> + <string name="cordn_admin_coordinator_silent">O coordenador não respondeu, então nada foi alterado. Tente novamente em instantes.</string> + <string name="cordn_admin_no_key_package">%1$s não publicou um pacote de chaves neste coordenador, então ainda não pode ser adicionado. Peça que abra o cordn neste coordenador primeiro.</string> + <string name="cordn_admin_no_self_remove">Você não pode remover a si mesmo de um grupo cordn.</string> + <string name="cordn_admin_no_welcome">Não foi possível dar a %1$s um meio de entrar no grupo, então a adição foi cancelada.</string> + <string name="cordn_admin_not_a_member">%1$s não está neste grupo.</string> + <string name="cordn_admin_someone">Essa pessoa</string> + <string name="cordn_admin_wrong_key_package">Este coordenador forneceu um pacote de chaves que pertence a outra pessoa, então %1$s não foi adicionado. Nada foi alterado.</string> + <string name="cordn_backup_contents_body">As chaves de criptografia dos seus grupos, seus pacotes de chaves e todas as mensagens desses grupos. Qualquer pessoa que abrir o arquivo pode ler as conversas. A senha é a única coisa que o protege depois que ele sai do Amethyst.</string> + <string name="cordn_backup_contents_title">O que há no arquivo</string> + <string name="cordn_backup_explainer">Sua nsec não recupera grupos cordn. Um grupo cordn existe como estado de criptografia neste dispositivo e como um fluxo ordenado em um coordenador que não consegue lê-lo — perca o dispositivo sem um backup e o grupo se vai, incluindo tudo o que já foi dito nele.</string> + <string name="cordn_backup_export">Exportar</string> + <string name="cordn_backup_exported">Backup salvo.</string> + <string name="cordn_backup_failed">Não funcionou. Verifique a senha e o arquivo.</string> + <string name="cordn_backup_passphrase">Senha</string> + <string name="cordn_backup_restore">Restaurar</string> + <string name="cordn_backup_restore_body">Restaurar substitui os grupos cordn deste dispositivo pelos do arquivo. Serve para um celular que substituiu outro — não é uma forma de usar os mesmos grupos em dois dispositivos ao mesmo tempo.</string> + <string name="cordn_backup_restore_confirm_body">Todos os grupos cordn atualmente neste dispositivo serão removidos e substituídos pelos do arquivo.\n\nNão restaure em um segundo celular enquanto o primeiro ainda estiver em uso. Ambos teriam as mesmas chaves de grupo e, assim que os dois enviarem uma mensagem, o grupo quebra para todos — silenciosamente e sem volta.</string> + <string name="cordn_backup_restore_confirm_title">Substituir os grupos cordn deste dispositivo?</string> + <string name="cordn_backup_restore_title">Restaurar</string> + <string name="cordn_backup_restored">Restaurado.</string> + <string name="cordn_chat_empty_description">Tudo o que for enviado aqui é criptografado para este grupo. O coordenador repassa as mensagens sem conseguir lê-las.</string> + <string name="cordn_chat_empty_title">Nenhuma mensagem ainda</string> + <string name="cordn_chat_unread_divider">Novas mensagens</string> + <string name="cordn_chat_yesterday">Ontem</string> + <string name="cordn_composer_hint">Mensagem</string> + <string name="cordn_coordinators_add_action">Adicionar</string> + <string name="cordn_coordinators_add_failed">Não foi possível abrir esse coordenador.</string> + <string name="cordn_coordinators_copy_key">Copiar a npub do coordenador</string> + <string name="cordn_coordinators_discover_action">Procurar coordenadores</string> + <string name="cordn_coordinators_discover_add">Adicionar</string> + <string name="cordn_coordinators_discover_explainer">Lê anúncios que seus relays já transportam. Nenhum coordenador é contatado, então nenhum deles fica sabendo que você procurou.</string> + <string name="cordn_coordinators_discover_failed">Não foi possível ler os anúncios</string> + <string name="cordn_coordinators_discover_none">Ninguém está se anunciando nos seus relays.</string> + <string name="cordn_coordinators_discover_seen">Anunciado há %1$s</string> + <string name="cordn_coordinators_discover_seen_on">Último anúncio: %1$s</string> + <plurals name="cordn_coordinators_discover_unheard"> + <item quantity="one">Nada encontrado, e %1$d dos seus relays não respondeu.</item> + <item quantity="other">Nada encontrado, e %1$d dos seus relays não responderam.</item> + </plurals> + <string name="cordn_coordinators_explainer">Um coordenador ordena as mensagens de um grupo e é o único que pode fazer isso. Perdê-lo significa perder a ordenação, e um segundo coordenador não espelha o primeiro — por isso são escolhas nomeadas por você, não relays intercambiáveis.</string> + <plurals name="cordn_coordinators_health_down"> + <item quantity="one">Sem resposta — %1$d chamada seguida falhou. Os grupos nele não estão sincronizando.</item> + <item quantity="other">Sem resposta — %1$d chamadas seguidas falharam. Os grupos nele não estão sincronizando.</item> + </plurals> + <string name="cordn_coordinators_health_ok">Respondendo.</string> + <string name="cordn_coordinators_health_retrying">Uma chamada falhou; tentando novamente.</string> + <string name="cordn_coordinators_health_unknown">Nada foi solicitado a ele ainda.</string> + <string name="cordn_coordinators_hide_older">Ocultar os que pararam de se anunciar</string> + <string name="cordn_coordinators_identify">Perguntar quem é</string> + <string name="cordn_coordinators_key">Chave do coordenador</string> + <string name="cordn_coordinators_label">Um nome para ele (seu, opcional)</string> + <string name="cordn_coordinators_label_note">Este nome é seu e fica neste dispositivo. Um coordenador não pode comprovar um nome, então nunca é solicitado um.</string> + <string name="cordn_coordinators_more">Mais ações</string> + <string name="cordn_coordinators_none">Nenhum coordenador ainda.</string> + <string name="cordn_coordinators_purge">Expurgar</string> + <string name="cordn_coordinators_purge_body">Remover mantém os grupos neste dispositivo, então adicionar o coordenador de volta os traz junto. Expurgar os exclui: as chaves dos grupos, as posições de leitura e seus pacotes de chaves são apagados, e esses grupos só podem ser reabertos com um novo convite.\n\nO coordenador não é avisado e mantém tudo o que já tinha.</string> + <string name="cordn_coordinators_purge_title">Expurgar este coordenador?</string> + <string name="cordn_coordinators_relays">%1$s</string> + <string name="cordn_coordinators_relays_label">Responde em</string> + <string name="cordn_coordinators_relays_more">%1$s +%2$d mais</string> + <string name="cordn_coordinators_remove">Remover</string> + <string name="cordn_coordinators_rename">Renomear</string> + <string name="cordn_coordinators_server">Diz ser: %1$s</string> + <string name="cordn_coordinators_server_claim">O nome e a versão de um coordenador são textos escolhidos por ele. A chave pública é a única coisa que o identifica.</string> + <string name="cordn_coordinators_server_silent">Respondeu, mas não informou nenhum nome.</string> + <string name="cordn_coordinators_show_all">Mostrar todos os %1$d</string> + <string name="cordn_coordinators_show_fewer">Mostrar menos</string> + <plurals name="cordn_coordinators_show_older"> + <item quantity="one">Mostrar %1$d que parou de se anunciar</item> + <item quantity="other">Mostrar %1$d que pararam de se anunciar</item> + </plurals> + <string name="cordn_coordinators_stale_note">Estes não se anunciam há mais de um mês. Criar um grupo em um que tenha sumido vai falhar.</string> + <string name="cordn_create_action">Criar grupo</string> + <plurals name="cordn_create_action_invite"> + <item quantity="one">Criar e convidar %1$d pessoa</item> + <item quantity="other">Criar e convidar %1$d pessoas</item> + </plurals> + <string name="cordn_create_add_member">Adicionar alguém</string> + <string name="cordn_create_admin">Admin</string> + <plurals name="cordn_create_admin_count"> + <item quantity="one">%1$d pessoa pode adicionar e remover</item> + <item quantity="other">%1$d pessoas podem adicionar e remover</item> + </plurals> + <string name="cordn_create_admin_only_me">Só eu gerencio este grupo</string> + <string name="cordn_create_admin_only_me_on">Só você pode convidar, remover membros ou alterar os detalhes do grupo. Você pode passar isso para outra pessoa depois.</string> + <string name="cordn_create_coordinator">Coordenador</string> + <string name="cordn_create_coordinator_change">Alterar</string> + <string name="cordn_create_coordinator_discover">Encontrar coordenadores</string> + <string name="cordn_create_coordinator_hide">Concluído</string> + <string name="cordn_create_coordinator_new">Outro coordenador</string> + <string name="cordn_create_coordinator_pubkey">Chave pública do coordenador (hex ou npub)</string> + <string name="cordn_create_coordinator_relays">Relays em que ele responde, um por linha</string> + <plurals name="cordn_create_coverage_all"> + <item quantity="one">Alcança a %1$d pessoa escolhida</item> + <item quantity="other">Alcança todas as %1$d pessoas escolhidas</item> + </plurals> + <string name="cordn_create_coverage_partial">Alcança %1$d de %2$d — as demais ficariam de fora</string> + <string name="cordn_create_coverage_unknown">Não verificado — escolhê-lo fará a consulta</string> + <string name="cordn_create_description">Descrição (opcional)</string> + <string name="cordn_create_egalitarian">Todos podem adicionar e remover</string> + <string name="cordn_create_egalitarian_note">Todos no grupo podem convidar, remover membros e alterar os detalhes. Qualquer membro pode nomear admins depois, o que encerra esse modo.</string> + <string name="cordn_create_explainer">Um grupo cordn é ordenado por um coordenador que você escolhe. A criação do grupo acontece neste dispositivo — o coordenador só fica sabendo que o grupo existe quando você convida alguém ou envia uma mensagem.</string> + <string name="cordn_create_failed">Não foi possível criar o grupo.</string> + <string name="cordn_create_name">Nome do grupo</string> + <string name="cordn_create_no_coordinator">Nenhum coordenador escolhido</string> + <string name="cordn_create_outcome_explainer">Cada convite é um Welcome deixado no coordenador, então eles podem falhar um a um. Ninguém foi notificado: as pessoas verão o convite na próxima vez que abrirem o app.</string> + <string name="cordn_create_outcome_failed">O coordenador recusou o convite</string> + <string name="cordn_create_outcome_link">Enviar um link</string> + <string name="cordn_create_outcome_no_welcome">Adicionado ao grupo, mas o convite não chegou — tente novamente pela tela do grupo</string> + <string name="cordn_create_outcome_open">Abrir o chat</string> + <string name="cordn_create_outcome_skipped">Sem chave aqui, então nenhum Welcome pôde ser deixado</string> + <string name="cordn_create_outcome_title">O grupo foi criado</string> + <string name="cordn_create_outcome_waiting">Aguardando a pessoa</string> + <string name="cordn_create_people_done">Concluído</string> + <string name="cordn_create_people_empty">Ninguém adicionado ainda. Um grupo pode ser criado vazio e os convites enviados depois, mas indicar as pessoas agora é o que mostra qual coordenador consegue alcançá-las.</string> + <string name="cordn_create_people_none">Ninguém ainda — toque para adicionar pessoas</string> + <string name="cordn_create_people_note">Indicar as pessoas agora é o que nos diz qual coordenador consegue alcançá-las. Cada uma recebe um Welcome deixado nesse coordenador, que ela recebe na próxima vez que abrir o app.</string> + <plurals name="cordn_create_reach_count"> + <item quantity="one">Alcançável em %1$d coordenador</item> + <item quantity="other">Alcançável em %1$d coordenadores</item> + </plurals> + <string name="cordn_create_reach_none">Sem chave em nenhum coordenador que você usa</string> + <string name="cordn_create_reach_unknown">Ainda não verificado</string> + <string name="cordn_create_step_admins">Quem pode adicionar e remover</string> + <string name="cordn_create_step_name">Dê um nome</string> + <string name="cordn_create_step_people">Quem participa</string> + <string name="cordn_create_step_where">Onde ele fica</string> + <string name="cordn_create_title">Novo grupo Cordn</string> + <plurals name="cordn_create_unreachable_count"> + <item quantity="one">%1$d pessoa não pode ser alcançada por nenhum coordenador que você usa</item> + <item quantity="other">%1$d pessoas não podem ser alcançadas por nenhum coordenador que você usa</item> + </plurals> + <string name="cordn_create_you">Você, sempre admin</string> + <string name="cordn_delete_confirm_body">Todos no grupo verão esta mensagem como retirada, e o Amethyst deixará de mostrar o texto. O coordenador ainda guarda a cópia criptografada que foi enviada, e quem já a leu, já leu.</string> + <string name="cordn_delete_confirm_title">Retirar mensagem</string> + <string name="cordn_delivery_accepted">Aceita pelo coordenador</string> + <string name="cordn_exposure_close">Entendi</string> + <string name="cordn_exposure_open">O que este coordenador pode ver</string> + <string name="cordn_group_info">Informações do grupo</string> + <string name="cordn_group_unavailable">Este grupo não está disponível. O coordenador dele pode ter sido removido.</string> + <string name="cordn_group_unavailable_detail">Adicione um coordenador nas Configurações para começar a usar grupos cordn.</string> + <plurals name="cordn_groups_count"> + <item quantity="one">%1$d grupo</item> + <item quantity="other">%1$d grupos</item> + </plurals> + <string name="cordn_groups_down_note">Nada está chegando destes grupos. O Amethyst continua tentando.</string> + <string name="cordn_groups_manage">Coordenadores</string> + <string name="cordn_groups_none">Nenhum grupo cordn ainda</string> + <string name="cordn_groups_none_detail">Um grupo cordn fica em um coordenador que guarda a lista de membros e repassa as mensagens, sem nunca ver o que alguém escreve.</string> + <string name="cordn_groups_start">Criar um</string> + <string name="cordn_info_add_member">Adicionar alguém</string> + <string name="cordn_info_add_member_none">Ninguém encontrado com esse nome.</string> + <string name="cordn_info_add_member_note">Só é possível adicionar quem publicou um pacote de chaves neste coordenador.</string> + <string name="cordn_info_add_member_placeholder">Nome, npub ou nome@domínio</string> + <string name="cordn_info_admin_badge">Admin</string> + <plurals name="cordn_info_admins_kept"> + <item quantity="one">O %1$d admin do grupo continua como está.</item> + <item quantity="other">Os %1$d admins do grupo continuam como estão.</item> + </plurals> + <string name="cordn_info_coordinator">Coordenador</string> + <string name="cordn_info_coordinator_key">Chave do coordenador</string> + <string name="cordn_info_coordinator_nprofile">Link do coordenador</string> + <string name="cordn_info_copy_nprofile">Copiar o nprofile do coordenador</string> + <string name="cordn_info_edit_details">Editar detalhes</string> + <string name="cordn_info_egalitarian">Este grupo não tem admins: todos podem adicionar, remover e renomear, permanentemente.</string> + <string name="cordn_info_epoch">Época</string> + <string name="cordn_info_gid">ID do grupo</string> + <string name="cordn_info_has_key_package">Pode ser adicionado neste coordenador</string> + <string name="cordn_info_members">Membros</string> + <string name="cordn_info_remove_confirm_body">%1$s deixa de receber as mensagens daqui. A pessoa mantém o que já leu, e isso não pode ser desfeito — ela teria de ser convidada novamente.</string> + <string name="cordn_info_remove_confirm_title">Remover do grupo?</string> + <string name="cordn_info_remove_member">Remover</string> + <string name="cordn_info_save">Salvar</string> + <string name="cordn_info_technical">Detalhes técnicos</string> + <string name="cordn_invitations_accept">Entrar</string> + <string name="cordn_invitations_decline">Recusar</string> + <string name="cordn_invitations_decline_warning">Recusar é permanente. Para voltar, será preciso ser convidado novamente.</string> + <string name="cordn_invitations_explainer">Verificado quando você abre esta tela, e em nenhum outro momento — perguntar a um coordenador se alguém convidou você revela a ele que você está aqui, por isso o Amethyst não faz isso em segundo plano.</string> + <string name="cordn_invitations_load_failed">Não foi possível ler os convites.</string> + <plurals name="cordn_invitations_members"> + <item quantity="one">%1$d pessoa já está neste grupo</item> + <item quantity="other">%1$d pessoas já estão neste grupo</item> + </plurals> + <string name="cordn_invitations_members_more">+%1$d mais</string> + <string name="cordn_invitations_no_coordinators">Você não tem nenhum coordenador aberto. Os convites do cordn chegam por um coordenador, então ainda não há onde procurar.</string> + <string name="cordn_invitations_none">Nenhum convite pendente.</string> + <string name="cordn_invitations_refresh">Verificar novamente</string> + <string name="cordn_invitations_skipped">Um convite foi deixado para outro dispositivo</string> + <string name="cordn_invitations_title">Convites Cordn</string> + <string name="cordn_invitations_unreachable">%1$s não respondeu</string> + <string name="cordn_invitations_via">Por %1$s</string> + <string name="cordn_keypackages_disclosure">Publicar assina um registro com a chave da sua própria conta. Esse coordenador passa a saber que esta conta existe e pode ser convidada — permanentemente, e quer alguém convide você ou não.</string> + <string name="cordn_keypackages_explainer">Outras pessoas só podem adicionar você a um grupo cordn usando um pacote de chaves que você publicou nesse coordenador. O cordn não tem outro lugar para guardá-lo, então sem nenhum publicado ninguém pode convidar você — e nada diz a elas o porquê.</string> + <string name="cordn_keypackages_failed">Não foi possível ler os pacotes de chaves.</string> + <string name="cordn_keypackages_last_resort_no">Nenhum pacote de último recurso. Quando os de uso único acabarem, os convites falharão.</string> + <string name="cordn_keypackages_last_resort_yes">Um pacote de último recurso está publicado, então ainda é possível convidar você quando os de uso único acabarem.</string> + <plurals name="cordn_keypackages_orphans"> + <item quantity="one">%1$d pacote que este dispositivo não consegue abrir</item> + <item quantity="other">%1$d pacotes que este dispositivo não consegue abrir</item> + </plurals> + <string name="cordn_keypackages_orphans_body">O coordenador entregará estes pacotes a quem for convidar você, mas a metade privada não está neste dispositivo — ela pertence a outra instalação. Se essa instalação não existir mais, um convite que use um deles gera um Welcome que ninguém jamais conseguirá abrir.</string> + <string name="cordn_keypackages_publish">Publicar um</string> + <string name="cordn_keypackages_publish_last_resort">Publicar de último recurso</string> + <plurals name="cordn_keypackages_summary"> + <item quantity="one">%1$d pacote de uso único disponível.</item> + <item quantity="other">%1$d pacotes de uso único disponíveis.</item> + </plurals> + <string name="cordn_keypackages_topup_note">Depois que você publicar aqui, o Amethyst repõe discretamente os pacotes conforme forem usados. Ele nunca publica o primeiro por você.</string> + <string name="cordn_keypackages_withdraw_all">Retirar todos</string> + <string name="cordn_keypackages_withdraw_orphans">Retirar esses</string> + <string name="cordn_link_request">Pedir para entrar neste grupo</string> + <string name="cordn_link_request_disclosure">Isto publica um pacote de chaves com a chave da sua própria conta e informa ao coordenador que você quer entrar neste grupo — quer alguém responda ou não.</string> + <string name="cordn_link_request_failed">Não foi possível pedir para entrar.</string> + <string name="cordn_link_request_sent">Pedido enviado. Um membro do grupo precisa adicionar você; o convite aparecerá em Convites Cordn.</string> + <string name="cordn_link_scan">Escanear</string> + <string name="cordn_media_download_failed">Não foi possível abrir esse arquivo.</string> + <string name="cordn_media_no_server">Nenhum servidor de mídia está definido para esta conta, então não há onde colocar o arquivo. Escolha um nas Configurações.</string> + <string name="cordn_media_unreadable">Não foi possível ler esse arquivo.</string> + <string name="cordn_media_upload_failed">Não foi possível enviar esse arquivo.</string> + <plurals name="cordn_member_count"> + <item quantity="one">%1$d membro</item> + <item quantity="other">%1$d membros</item> + </plurals> + <string name="cordn_message_deleted">Mensagem excluída</string> + <string name="cordn_message_details_coordinator">Coordenador</string> + <string name="cordn_message_details_cursor">Cursor</string> + <string name="cordn_message_details_sent_at">Enviada</string> + <string name="cordn_message_details_title">Detalhes da mensagem</string> + <string name="cordn_message_edited">editada</string> + <string name="cordn_pinned_by">Fixada por %1$s</string> + <plurals name="cordn_pinned_count"> + <item quantity="one">%1$d mensagem fixada neste grupo</item> + <item quantity="other">%1$d mensagens fixadas neste grupo</item> + </plurals> + <string name="cordn_pinned_next">Próxima mensagem fixada</string> + <string name="cordn_pinned_previous">Mensagem fixada anterior</string> + <string name="cordn_pinned_show_all">Mostrar todas as mensagens fixadas</string> + <string name="cordn_pinned_title">Mensagens fixadas</string> + <string name="cordn_reactions_title">Reações</string> + <string name="cordn_requests_accept">Adicionar</string> + <string name="cordn_requests_admin_only">Só um admin pode aceitar. Em um grupo sem admins, isso vale para todos.</string> + <string name="cordn_requests_check">Verificar pedidos</string> + <string name="cordn_requests_decline">Dispensar</string> + <string name="cordn_requests_failed">Não foi possível ler os pedidos.</string> + <string name="cordn_requests_none">Ninguém está esperando para entrar.</string> + <string name="cordn_requests_title">Pedidos para entrar</string> + <string name="cordn_send">Enviar</string> + <string name="cordn_send_failed">Não foi enviada.</string> + <string name="cordn_send_no_session">O coordenador deste grupo não está aberto, então ainda não é possível enviar nada.</string> + <string name="cordn_share_copied">Copiado</string> + <string name="cordn_share_copy">Copiar link</string> + <string name="cordn_share_explainer">Qualquer pessoa com este link pode pedir para entrar. Um admin ainda precisa aceitá-la.</string> + <string name="cordn_share_title">Compartilhar este grupo</string> + <string name="cordn_voice_play">Reproduzir mensagem de voz</string> + <string name="cordn_voice_record">Gravar uma mensagem de voz</string> + <string name="cordn_voice_stop">Parar e enviar</string> </resources> diff --git a/commonsUI/src/commonMain/composeResources/values-sv-rSE/strings.xml b/commonsUI/src/commonMain/composeResources/values-sv-rSE/strings.xml index 6a44e20832..c63d123853 100644 --- a/commonsUI/src/commonMain/composeResources/values-sv-rSE/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-sv-rSE/strings.xml @@ -166,6 +166,152 @@ <string name="road_event_traffic_jam">Trafikstockning</string> <string name="road_event_unknown">Väghändelse</string> <!-- NIP-CC geocaching (kinds 37516 / 7516) --> + <string name="geocache_unnamed">Geocache</string> + <string name="geocache_type_traditional">Traditionell</string> + <string name="geocache_type_multi">Multicache</string> + <string name="geocache_type_mystery">Mystery</string> + <string name="geocache_size_micro">Mikro</string> + <string name="geocache_size_small">Liten</string> + <string name="geocache_size_regular">Normal</string> + <string name="geocache_size_large">Stor</string> + <string name="geocache_size_other">Annan storlek</string> + <string name="geocache_difficulty_short">D%1$d</string> + <string name="geocache_terrain_short">T%1$d</string> + <string name="geocache_first_to_find">Först att hitta (FTF)</string> + <string name="geocache_is_art">Konstverk</string> + <string name="geocache_mission">Key Quest</string> + <string name="geocache_needs_verification">Verifierade fynd</string> + <string name="geocache_claimed">Tagen</string> + <string name="geocache_archived">Arkiverad</string> + <string name="geocache_hint_tap_to_reveal">Kört fast? Tryck för att visa ledtråden</string> + <string name="geocache_hint_other_reading">Går den inte att läsa? Tryck för den andra versionen</string> + <string name="geocache_nearby">I närheten</string> + <string name="geocache_found_it">Hittad!</string> + <string name="geocache_verified_find">Verifierat fynd</string> + <string name="geocache_invalid_proof">Beviset stämmer inte</string> + <string name="geocache_loading">Läser in cache…</string> + <string name="geocache_hidden_by">Gömd av %1$s</string> + <string name="geocache_navigate">Navigera</string> + <string name="geocache_copy_coordinates">Kopiera koordinater</string> + <string name="geocache_coordinates_copied">Koordinater kopierade</string> + <string name="geocache_photos_section">Foton</string> + <string name="geocache_mission_section">Key Quest-uppdrag</string> + <string name="geocache_logs_section">Loggar</string> + <string name="geocache_no_logs">Ingen har loggat den här cachen ännu. Bli först.</string> + <string name="geocache_i_found_it">Jag hittade den</string> + <string name="geocache_didnt_find_it">Hittade den inte</string> + <string name="geocache_add_note">Lägg till en anteckning</string> + <string name="geocache_needs_maintenance">Rapportera behov av underhåll</string> + <string name="geocache_add_to_hunt">Lägg till i en jakt</string> + <string name="geocache_hunt_new">Ny jakt…</string> + <string name="geocache_hunt_none_yet">Du har inte skapat någon jakt ännu.</string> + <string name="geocache_hunt_already_on">Redan med i den här jakten</string> + <plurals name="geocache_caches_here"> + <item quantity="one">%1$d cache här</item> + <item quantity="other">%1$d cacher här</item> + </plurals> + <string name="geocache_photo_remove">Ta bort det här fotot</string> + <string name="geocache_archived_notice">Den här cachen har arkiverats. Den är inte längre i spel.</string> + <string name="geocache_ftf_won_by">FTF tagen av %1$s</string> + <string name="geocache_ftf_available">FTF är fortfarande ledig</string> + <string name="geocache_owner_actions">Ägare</string> + <string name="geocache_owner_edit">Redigera den här cachen</string> + <string name="geocache_owner_archive">Arkivera den här cachen</string> + <string name="geocache_owner_archive_confirm">Arkivera den. Hittare ser att den är ur spel, och beskrivningen går fortfarande att läsa.</string> + <string name="geocache_owner_lock_ftf">Lås den första hittaren</string> + <string name="geocache_owner_lock_ftf_explain">Publicerar en F-tagg som anger %1$s. Detta är permanent.</string> + <string name="geocache_owner_no_finders">Inga verifierade fynd ännu, så det finns ingen att låsa.</string> + <string name="geocache_owner_show_qr">Visa verifieringskoden</string> + <string name="geocache_owner_qr_warning">Skriv ut den och lägg den i cachen. Amethyst sparar den inte — om du tappar bort den kan hittare inte längre bevisa att de var där.</string> + <string name="geocache_log_type_dnf">Hittade inte (DNF)</string> + <string name="geocache_log_type_note">Anteckning</string> + <string name="geocache_log_type_maintenance">Behöver underhåll</string> + <string name="geocache_log_title">Logga ett fynd</string> + <string name="geocache_log_cache_label">Cache</string> + <string name="geocache_log_your_log">Din logg</string> + <string name="geocache_log_placeholder">Hur gick det?</string> + <string name="geocache_log_add_photo">Lägg till ett foto</string> + <string name="geocache_log_mission_answer">Ditt Key Quest-svar</string> + <string name="geocache_log_proof_section">Bevis på närvaro</string> + <string name="geocache_log_scan_code">Skanna koden i cachen</string> + <string name="geocache_log_scan_again">Skanna en annan kod</string> + <string name="geocache_log_proof_verified">Verifierat — du var vid cachen</string> + <string name="geocache_log_proof_explain">Signeras en gång med koden i lådan. Nyckeln sparas aldrig och lämnar aldrig den här skärmen.</string> + <string name="geocache_log_proof_bad_code">Det där är ingen verifieringskod.</string> + <string name="geocache_log_proof_wrong_cache">Den koden hör till en annan cache.</string> + <string name="geocache_log_proof_optional">Den här cachen kräver inget bevis, så du kan logga den direkt.</string> + <string name="geocache_log_no_location_attached">Din egen plats bifogas inte till den här loggen.</string> + <string name="geocache_log_post">Publicera</string> + <string name="geocache_log_comment_title">Lägg till en logg</string> + <string name="geocache_tab_nearby">I närheten</string> + <string name="geocache_tab_map">Karta</string> + <string name="geocache_tab_hunts">Jakter</string> + <string name="geocache_tab_finds">Fynd</string> + <string name="geocache_tab_mine">Mina</string> + <string name="geocache_filter_unfound">Ej hittade</string> + <string name="geocache_filter_difficulty">D ≤ %1$d</string> + <string name="geocache_filter_terrain">T ≤ %1$d</string> + <string name="geocache_filter_has_prize">Har ett pris</string> + <string name="geocache_filter_verified_only">Bevis krävs</string> + <string name="geocache_use_my_location">Använd min plats</string> + <string name="geocache_sorted_by_distance">Sorterat efter avstånd</string> + <string name="geocache_sorted_by_recent">Nyaste först</string> + <string name="geocache_hide_a_cache">Göm en cache</string> + <string name="geocache_empty_nearby">Inga cacher här ännu. Prova en bredare följlista, eller göm den första.</string> + <string name="geocache_empty_finds">Du har inte loggat något fynd ännu.</string> + <string name="geocache_empty_mine">Du har inte gömt någon cache ännu.</string> + <string name="geocache_empty_hunts">Inga jakter ännu.</string> + <string name="geocache_stat_finds">Fynd</string> + <string name="geocache_stat_ftf">FTF</string> + <string name="geocache_stat_verified">Verifierade</string> + <string name="geocache_stat_hidden">Gömda</string> + <string name="geocache_map_recenter">Centrera på mig</string> + <string name="geocache_map_long_press">Tryck länge på kartan för att gömma en cache där</string> + <string name="geocache_map_open">Öppna</string> + <string name="geocache_new_section_where">Var</string> + <string name="geocache_new_section_what">Vad</string> + <string name="geocache_new_section_extras">Extra</string> + <string name="geocache_new_section_proof">Bevis</string> + <string name="geocache_new_section_relays">Reläer</string> + <string name="geocache_new_location_warning">Detta publicerar en offentlig plats med cirka 5 meters noggrannhet. Alla på Nostr kan se den, och en publicerad händelse kan inte återkallas.</string> + <string name="geocache_new_pick_location">Välj platsen</string> + <string name="geocache_new_ladder">Publicerar %1$s, plus grövre celler ned till 3 tecken för närhetssökning.</string> + <string name="geocache_new_name">Namn</string> + <string name="geocache_new_description">Beskrivning</string> + <string name="geocache_new_type">Typ</string> + <string name="geocache_new_size">Storlek</string> + <string name="geocache_new_difficulty">Svårighetsgrad</string> + <string name="geocache_new_terrain">Terräng</string> + <string name="geocache_new_hint">Ledtråd</string> + <string name="geocache_new_hint_preview">Hittare kommer att se</string> + <string name="geocache_new_hint_scramble">Kryptera ledtråden (ROT13)</string> + <string name="geocache_new_mission">Key Quest-uppdrag</string> + <string name="geocache_new_modifiers">Tillägg</string> + <string name="geocache_new_require_proof">Kräv bevis på närvaro</string> + <string name="geocache_new_proof_ready">En verifieringsnyckel har skapats för den här cachen.</string> + <string name="geocache_new_log_relays">Var hittare ska logga</string> + <string name="geocache_new_publish">Publicera</string> + <string name="geocache_new_needs_name">Ge cachen ett namn.</string> + <string name="geocache_new_needs_location">Välj var cachen är gömd.</string> + <string name="geocache_hunt_progress">%1$d av %2$d hittade</string> + <string name="geocache_hunt_start">Starta jakten</string> + <plurals name="geocache_hunt_caches"> + <item quantity="one">%1$d cache</item> + <item quantity="other">%1$d cacher</item> + </plurals> + <string name="geocache_hunt_title">Titel</string> + <string name="geocache_hunt_description">Beskrivning</string> + <string name="geocache_hunt_banner">Bannerbild</string> + <string name="geocache_hunt_pick_caches">Cacher, i ordning</string> + <string name="geocache_hunt_add_cache">Lägg till en cache</string> + <string name="geocache_hunt_move_up">Flytta upp</string> + <string name="geocache_hunt_move_down">Flytta ned</string> + <string name="geocache_hunt_remove">Ta bort</string> + <string name="geocache_hunt_theme">Sidtema</string> + <string name="geocache_hunt_map_style">Kartstil</string> + <string name="geocache_hunt_needs_title">Ge jakten en titel.</string> + <string name="geocache_hunt_needs_caches">Lägg till minst en cache.</string> + <string name="geocache_publish_failed">Kunde inte publicera. Kontrollera reläanslutningen och försök igen.</string> <string name="podcast_value_zap_split_hint">Zaps till detta fördelas mellan:</string> <string name="podcast_value_split_percent">%1$d%%</string> <string name="podcast_value_for_value">Värde -för-värde</string> @@ -188,6 +334,51 @@ </plurals> <string name="profile_banner">Profil Banner</string> <string name="point_to_the_qr_code">Peka mot QR-koden</string> + <string name="qr_scanner_camera_rationale">Amethyst behöver kameran för att skanna QR-koder. Inget spelas in eller laddas upp — bilderna avkodas på enheten och kastas sedan.</string> + <string name="qr_scanner_grant_camera">Tillåt kamera</string> + <string name="qr_scanner_camera_blocked">Kameraåtkomst är avstängd för Amethyst. Du kan slå på den igen i Androids inställningar.</string> + <string name="qr_scanner_open_settings">Öppna inställningar</string> + <string name="qr_scanner_unavailable">QR-avkodaren kunde inte starta på den här enheten. Du kan fortfarande klistra in koden som text.</string> + <string name="qr_scanner_torch_on">Tänd lampan</string> + <string name="qr_scanner_torch_off">Släck lampan</string> + <string name="qr_scanner_dark_hint">Det är mörkt — prova lampan</string> + <string name="qr_scanner_scan_image">Skanna en bild</string> + <string name="qr_scanner_paste">Klistra in</string> + <string name="qr_scanner_no_code_in_image">Ingen QR-kod hittades i bilden</string> + <string name="qr_scanner_clipboard_empty">Det finns inget i urklipp att skanna</string> + <string name="qr_scanner_pick_one">Tryck på koden du vill ha</string> + <plurals name="qr_scanner_sequence_progress"> + <item quantity="one">Fångade %1$s av %2$s del — fortsätt</item> + <item quantity="other">Fångade %1$s av %2$s delar — fortsätt</item> + </plurals> + <string name="qr_scanner_zoom_reset">Återställ zoom</string> + <string name="qr_scanner_unsupported_title">Amethyst kan inte öppna den här koden</string> + <string name="qr_scanner_unsupported_secret">Den här koden innehåller en privat nyckel eller en parningshemlighet, så innehållet visas inte här. Använd skärmen som förväntar sig den.</string> + <string name="qr_scanner_kind_nsec">Det här är en privat nyckel. Klistra in den på inloggningsskärmen i stället.</string> + <string name="qr_scanner_kind_wallet">Det här är en plånboksanslutning. Lägg till den under Plånboksinställningar.</string> + <string name="qr_scanner_kind_bunker">Det här är en fjärrsignatäradress. Amethyst delar ut sådana så att andra appar kan be den signera — den kan inte logga in med en.</string> + <string name="qr_scanner_kind_signer">Det här är en app som vill ansluta till din signatär. Öppna den från NIP-46-signatärskärmen.</string> + <string name="qr_scanner_kind_lightning">Det här är en Lightning-faktura.</string> + <string name="qr_scanner_kind_cashu">Det här är en Cashu-token.</string> + <string name="qr_scanner_kind_web">Det här är en webblänk.</string> + <string name="qr_scanner_kind_text">Det här är vanlig text, inte en Nostr-kod.</string> + <string name="qr_scanner_kind_nostr_unsupported">Det här är en Nostr-kod, men inte en som den här skärmen kan öppna.</string> + <string name="qr_scanner_label_profile">Profil</string> + <string name="qr_scanner_label_note">Anteckning</string> + <string name="qr_scanner_label_nostr">Nostr-kod</string> + <string name="qr_scanner_label_relay">Relä</string> + <string name="qr_scanner_label_nsec">Privat nyckel</string> + <string name="qr_scanner_label_wallet">Plånboksanslutning</string> + <string name="qr_scanner_label_bunker">Fjärrsignatäradress</string> + <string name="qr_scanner_label_signer">Anslutningsbegäran till signatär</string> + <string name="qr_scanner_label_lightning">Lightning-faktura</string> + <string name="qr_scanner_label_cashu">Cashu-token</string> + <string name="qr_scanner_label_web">Webblänk</string> + <string name="qr_scanner_label_text">Vanlig text</string> + <string name="qr_scanner_open_link">Öppna länk</string> + <string name="qr_scanner_copy">Kopiera</string> + <string name="qr_scanner_copied">Kopierat</string> + <string name="qr_scanner_try_again">Skanna igen</string> <string name="show_qr">Visa QR</string> <string name="profile_image">Profilbild</string> <string name="your_profile_image">Din profilbild</string> @@ -638,17 +829,70 @@ <string name="workout_hours">Timmar</string> <string name="workout_minutes">Minuter</string> <string name="workout_seconds">Sekunder</string> + <string name="workout_suggestion_connect_title">Använd träningspassen du redan har registrerat</string> + <string name="workout_suggestion_connect_message">Anslut Health Connect (Samsung Health, Google Fit, Fitbit, Garmin…) för att bygga Min träning, din egen träningssammanfattning, och för att fylla i det här passet från ett som din klocka redan har registrerat i stället för att skriva in varje siffra för hand.</string> <string name="workout_suggestion_connect_button">Anslut</string> <string name="workout_suggestion_distance_km">%1$s km</string> <string name="workout_from_health_connect">Från Health Connect</string> <string name="workout_suggestion_connect_details">Vad Amethyst läser</string> <string name="health_connect_rationale_title">Health Connect och Amethyst</string> + <string name="health_connect_rationale_headline">Amethyst använder Health Connect för att bygga Min träning — din egen träningssammanfattning, på din telefon.</string> + <string name="health_connect_rationale_intro">Min träning (meny → Du → Min träning) visar hur du har tränat: hur mycket du gjorde den här veckan jämfört med förra, hur din tid fördelas mellan löpning, cykling, promenader, simning och gym, dina bästa prestationer, hur många dagar du tränade och din nuvarande svit. Amethyst bygger den bilden från träningspassen som din klocka eller träningsapp redan har sparat i Health Connect, tillsammans med pass du har loggat i Amethyst. Allt räknas ut på din telefon och visas för dig.</string> <string name="health_connect_rationale_what_title">Vad Amethyst läser och varför</string> + <string name="health_connect_rationale_exercise">Träning · vilken aktivitet du utförde, när den började och hur länge den varade. Det här är själva passet: det styr antalet pass, din träningstid, uppdelningen per aktivitet, dina aktiva dagar och din svit.</string> + <string name="health_connect_rationale_distance">Distans · hur långt du tog dig, visat som din distans den här veckan jämfört med förra, ditt veckosnitt, din distans per aktivitet och din längsta distans.</string> + <string name="health_connect_rationale_calories">Aktiva och totala kalorier · energin ett pass förbrände, visat som din energi den här veckan och hur den står sig mot förra veckan. Aktiva kalorier används när din källa registrerar dem; totala kalorier används som reserv för källor som bara registrerar total energi.</string> + <string name="health_connect_rationale_heart_rate">Puls · genomsnitt och maxvärde för varje pass, sammanslaget till din genomsnittliga och högsta puls för perioden. Det är så du skiljer en lätt vecka från en tuff med samma distans.</string> + <string name="health_connect_rationale_steps">Steg · antalet steg under en löptur, promenad eller vandring, visat som ditt veckosnitt för steg och ditt högsta antal steg.</string> + <string name="health_connect_rationale_elevation">Höjdmeter · hur mycket du klättrade, visat som ditt veckosnitt för stigning och din största stigning. Det är det som skiljer en platt vecka från en kuperad.</string> <string name="health_connect_rationale_limits_title">Vad Amethyst inte gör</string> + <string name="health_connect_rationale_limit_window">Läser bara träningspass från de senaste 4 veckorna, och bara medan Min träning eller passredigeraren är öppen. Läser aldrig i bakgrunden.</string> <string name="health_connect_rationale_limit_write">Skriver aldrig något till Health Connect och begär aldrig din träningsrutt, plats eller någon annan typ av hälsodata.</string> + <string name="health_connect_rationale_limit_publish">Skickar aldrig dina hälsodata någonstans. Min träning räknas ut på din telefon och stannar där. Om du senare vill publicera ett pass trycker du på Dela för det passet, läser inlägget som Amethyst fyllt i och publicerar det själv — ett pass i taget, aldrig automatiskt.</string> + <string name="health_connect_rationale_limit_optional">Hela funktionen är valfri. Återkalla behörigheterna i Health Connect när som helst — Min träning går då tillbaka till sin uppmaning — eller stäng av förslagen i redigeraren under Inställningar → Skrivinställningar. Resten av Amethyst fortsätter att fungera.</string> <string name="health_connect_rationale_privacy_policy">Läs hela integritetspolicyn</string> + <string name="my_fitness_title">Min träning</string> + <string name="my_fitness_window">Senaste 4 veckorna</string> + <string name="my_fitness_loading_metrics">Läser in distans, kalorier och puls…</string> + <string name="my_fitness_window_note">De senaste fyra hela veckorna av din träning: passen du har publicerat, plus allt Health Connect registrerar på den här telefonen. Amethyst räknar ut sammanfattningen på enheten och publicerar ingenting av den.</string> + <string name="my_fitness_this_week">Den här veckan</string> + <string name="my_fitness_vs_last_week">jämfört med förra veckan</string> + <string name="my_fitness_weekly_average">Veckosnitt</string> + <string name="my_fitness_workouts">Pass</string> + <string name="my_fitness_active_days">Aktiva dagar</string> + <string name="my_fitness_streak">Dagar i rad</string> + <string name="my_fitness_time">Tid</string> <!-- Workout duration totals. %1$d is hours, %2$d is minutes; reorder and re-abbreviate freely — these are English abbreviations for hour/minute/second. --> + <string name="my_fitness_duration_hours_minutes">%1$d h %2$d min</string> + <string name="my_fitness_duration_minutes">%1$d min</string> + <string name="my_fitness_duration_seconds">%1$d s</string> + <string name="my_fitness_distance">Distans</string> + <string name="my_fitness_calories">Kalorier</string> + <string name="my_fitness_steps">Steg</string> + <string name="my_fitness_elevation">Stigning</string> + <string name="my_fitness_avg_heart_rate">Snittpuls</string> + <string name="my_fitness_max_heart_rate">Maxpuls</string> + <string name="my_fitness_by_activity">Per aktivitet</string> + <string name="my_fitness_bests">Bästa prestationer</string> + <string name="my_fitness_best_longest_distance">Längsta distans</string> + <string name="my_fitness_best_longest_duration">Längsta pass</string> + <string name="my_fitness_best_biggest_climb">Största stigning</string> + <string name="my_fitness_best_most_steps">Flest steg</string> + <string name="my_fitness_best_highest_heart_rate">Högsta puls</string> + <string name="my_fitness_recent">Senaste passen</string> + <string name="my_fitness_empty">Inget loggat de senaste 4 veckorna. Pass du publicerar, och de som din klocka registrerar, visas här.</string> + <string name="my_fitness_connect_title">Följ din träning</string> + <string name="my_fitness_connect_message">Anslut Health Connect för att se dina veckosummor, utvecklingen vecka för vecka, bästa prestationer och träningssvit. Allt stannar på din telefon.</string> + <string name="my_fitness_connect_banner">Anslut Health Connect för att lägga till passen din klocka registrerar, med puls, steg och stigning.</string> + <string name="my_fitness_connect_button">Anslut</string> + <string name="my_fitness_share">Dela det här passet</string> + <string name="my_fitness_unit_bpm">bpm</string> + <string name="my_fitness_unit_kcal">kcal</string> + <string name="my_fitness_unit_km">km</string> + <string name="my_fitness_unit_mi">mi</string> + <string name="my_fitness_unit_m">m</string> + <string name="my_fitness_unit_ft">ft</string> <string name="nsite_none_found">Inga nSites hittades ännu.</string> <string name="browser_reload">Ladda om</string> <string name="browser_unsupported">Den inbyggda webbläsaren kräver Android 11 eller senare.</string> @@ -1084,6 +1328,8 @@ <string name="poll_closing_date_time">Stängningsdatum och tid</string> <string name="poll_closing_in">Omröstningen stänger om %1$s</string> <string name="looking_for_event">Letar efter Event %1$s</string> + <string name="looking_for_event_title">Läser in</string> + <string name="looking_for_private_event">Väntar på att dina privata meddelanden ska läsas in…</string> <string name="send_zap">Skicka Zap</string> <string name="custom_zaps_add_a_message">Lägg till ett offentligt meddelande</string> <string name="custom_zaps_add_a_message_private">Lägg till ett privat meddelande</string> @@ -1223,6 +1469,8 @@ <!-- Elapsed call time. %1$d is minutes and %2$d seconds in the short form; in the long form %1$d is hours, %2$d minutes and %3$d seconds. Separator and field order are the translator's to change. --> + <string name="call_duration_minutes_seconds">%1$02d:%2$02d</string> + <string name="call_duration_hours_minutes_seconds">%1$d:%2$02d:%3$02d</string> <string name="call_dismiss">Avvisa</string> <string name="call_switch_camera">Byt kamera</string> <string name="call_voice">Röstsamtal</string> @@ -1357,6 +1605,14 @@ <string name="user_preferences">Användarinställningar</string> <string name="settings_search_placeholder">Sök inställningar</string> <string name="settings_search_no_results">Inga inställningar hittades för ”%1$s”</string> + <string name="marmot_preview_group_updated">Gruppen uppdaterad</string> + <string name="marmot_preview_media">Bilaga</string> + <string name="marmot_preview_no_text">Meddelande</string> + <string name="marmot_preview_with_sender">%1$s: %2$s</string> + <string name="marmot_empty_create_action">Skapa en grupp</string> + <string name="marmot_invite_device_banner">Marmot-inbjudningar går till en annan enhet och kan inte öppnas här.</string> + <string name="marmot_invite_device_banner_dismiss">Avfärda</string> + <string name="marmot_invite_device_publish">Publicera från den här enheten</string> <string name="reset_marmot_confirm_title">Återställa Marmot-tillstånd?</string> <string name="reset_marmot_confirm_body">Detta tar permanent bort alla Marmot-gruppchattar, meddelandehistorik och MLS-nycklar på den här enheten för det aktuella kontot. Deltagare meddelas inte och kan fortfarande se dig i grupper tills de bekräftar nästa gång. Detta går inte att ångra. Ett nytt KeyPackage publiceras nästa gång appen synkroniseras.</string> <string name="reset_marmot_confirm_action">Återställ</string> @@ -1627,6 +1883,7 @@ <string name="wallet_no_transactions_for_filter">Inga transaktioner matchar detta filter</string> <string name="wallet_loading">Laddar…</string> <string name="wallet_incoming">Mottagen</string> + <string name="wallet_open_zapped_note">Öppna zappad anteckning</string> <string name="wallet_outgoing">Skickad</string> <string name="wallet_refresh">Uppdatera</string> <string name="wallet_filter_all">Alla</string> @@ -1943,6 +2200,8 @@ <string name="video_player_settings_action_pip_description">Spela upp videon i ett flytande fönster (dolt om det inte stöds)</string> <string name="video_player_settings_action_cast">Casta till enhet</string> <string name="video_player_settings_action_cast_description">Skicka videon till en Chromecast-mottagare på ditt Wi-Fi (dolt för lokala filer)</string> + <string name="video_player_settings_action_captions">Undertexter</string> + <string name="video_player_settings_action_captions_description">Slå på eller av undertexter (dolt när videon saknar sådana)</string> <string name="audio_visualizer_settings_description">Välj animeringen som visas när ljudanteckningar spelas upp.</string> <string name="audio_visualizer_off">Av</string> <string name="audio_visualizer_bars">Spektrumstaplar</string> @@ -2630,6 +2889,7 @@ <string name="community_rules_violation_wot_gate_failed">Du uppfyller inte den här gemenskapens krav på förtroendenät.</string> <string name="community_rules_violation_stale_rules">Det senaste dokumentet med gemenskapsregler avvisades som föråldrat.</string> <string name="marmot_groups_title">Marmot-grupper</string> + <string name="cordn_groups_title">Cordn-grupper</string> <string name="marmot_create_group">Skapa grupp</string> <string name="marmot_create_group_title">Skapa Marmot-grupp</string> <string name="marmot_create_group_footer">En ny MLS-grupp kommer att skapas. Du kan lägga till medlemmar efteråt.</string> @@ -2921,13 +3181,18 @@ <string name="app_notification_replies_channel_description">Meddelar dig när någon svarar på ditt inlägg</string> <string name="app_notification_replies_channel_name">Svar</string> <string name="app_notification_replies_summary">Nya svar</string> + <string name="app_notification_reply_failed">Inte skickat</string> <string name="app_notification_reply_label">Svara</string> + <string name="app_notification_reply_retry">Försök igen</string> + <string name="app_notification_reply_sending">Skickar…</string> + <string name="app_notification_reply_unconfirmed">Skickar fortfarande…</string> <string name="app_notification_reposts_channel_description">Meddelar dig när någon repostar ditt inlägg</string> <string name="app_notification_reposts_channel_message">%1$s repostade ditt inlägg</string> <string name="app_notification_reposts_channel_name">Reposter</string> <string name="app_notification_reposts_summary">Nya reposter</string> <string name="app_notification_scheduled_posts_channel_description">Aviseringar när ett schemalagt inlägg publiceras eller misslyckas att publicera.</string> <string name="app_notification_scheduled_posts_channel_name">Schemalagda inlägg</string> + <string name="app_notification_shortcut_account_removed">Det här kontot har tagits bort från Amethyst.</string> <string name="app_notification_zaps_channel_description">Meddela dig när någon zappar dig</string> <string name="app_notification_zaps_channel_message">%1$s sats</string> <string name="app_notification_zaps_channel_message_for">till %1$s</string> @@ -3128,6 +3393,10 @@ <string name="call_unmute">Ljud på</string> <string name="callback_url_not_found_in_the_user_s_lightning_address_server_configuration_with_user">Callback URL hittades inte från %1$s's svar</string> <string name="cancel">Avbryt</string> + <string name="captions_off">Av</string> + <string name="captions_turn_off">Stäng av undertexter</string> + <string name="captions_turn_on">Slå på undertexter</string> + <string name="captions_unnamed_track">Undertexter</string> <string name="cashu">Cashu Token</string> <string name="cashu_checking_mint">Kontrollerar minten…</string> <string name="cashu_failed_redemption">Kunde inte lösa in Cashu</string> @@ -3605,10 +3874,12 @@ <string name="kind_adventure_prologue">Äventyrsprolog</string> <string name="kind_adventure_reading">Äventyrsläsning</string> <string name="kind_adventure_scene">Äventyrsscen</string> + <string name="kind_app_curation_set">Applista</string> <string name="kind_app_recommendations">Apprekommendationer</string> <string name="kind_appointment">Möte</string> <string name="kind_apps">Appar</string> <string name="kind_appt_rsvp">OSA för möte</string> + <string name="kind_article_curation_set">Artikellista</string> <string name="kind_audio_header">Ljudhuvud</string> <string name="kind_audio_track">Ljudspår</string> <string name="kind_authored_podcasts">Skapade poddar</string> @@ -3621,6 +3892,7 @@ <string name="kind_blossom_auth">Blossom-autentisering</string> <string name="kind_blossom_servers">Blossom-servrar</string> <string name="kind_bookmark_list">Bokmärkeslista</string> + <string name="kind_bookmark_set">Bokmärkessamling</string> <string name="kind_broadcast_relays">Broadcast-relayer</string> <string name="kind_calendar">Kalender</string> <string name="kind_channel_definition">Kanaldefinition</string> @@ -3658,6 +3930,7 @@ <string name="kind_emoji_packs">Emoji-paket</string> <string name="kind_ephemeral_chat">Tillfällig chatt</string> <string name="kind_ephemeral_chatrooms">Tillfälliga chattrum</string> + <string name="kind_favorite_algo_feeds">Favoritflöden</string> <string name="kind_favorite_podcasts">Favoritpoddar</string> <string name="kind_favorite_relays">Favoritrelayer</string> <string name="kind_file_headers">Filhuvuden</string> @@ -3666,28 +3939,36 @@ <string name="kind_follow_packs">Följ-paket</string> <string name="kind_geohash_follows">Geohash-följer</string> <string name="kind_gift_wraps">Presentinslagningar</string> + <string name="kind_git_authors">Git-författare</string> <string name="kind_git_issue">Git-ärende</string> <string name="kind_git_patch">Git-patch</string> <string name="kind_git_pr">Pull Request</string> <string name="kind_git_pr_update">PR-uppdatering</string> <string name="kind_git_reply">Git-svar</string> <string name="kind_git_repo">Git-repository</string> + <string name="kind_git_repositories">Git-förråd</string> <string name="kind_git_status_applied">Git-status: Tillämpad</string> <string name="kind_git_status_closed">Git-status: Stängd</string> <string name="kind_git_status_draft">Git-status: Utkast</string> <string name="kind_git_status_open">Git-status: Öppen</string> + <string name="kind_good_wiki_authors">Betrodda wikiförfattare</string> + <string name="kind_good_wiki_relays">Betrodda wikireläer</string> <string name="kind_hashtag_follows">Följda hashtaggar</string> <string name="kind_highlights">Höjdpunkter</string> <string name="kind_http_auth">HTTP-autentisering</string> <string name="kind_index_relay_list">Indexrelälista</string> + <string name="kind_interest_set">Intressen</string> <string name="kind_live_chats">Livechattar</string> <string name="kind_live_streams">Livesändningar</string> + <string name="kind_media_follows">Mediaföljningar</string> + <string name="kind_media_starter_pack">Startpaket för media</string> <string name="kind_medical_data">Medicinska data</string> <string name="kind_meeting_room">Mötesrum</string> <string name="kind_meeting_space">Mötesutrymme</string> <string name="kind_music_playlist">Musikspellista</string> <string name="kind_music_track">Musikspår</string> <string name="kind_mute_list">Tystnadslista</string> + <string name="kind_mute_set_for_kind">Tystat för typ %1$s</string> <string name="kind_named_bookmarks">Namngivna bokmärken</string> <string name="kind_nests_servers">Nests-servrar</string> <string name="kind_nip">NIP</string> @@ -3702,6 +3983,7 @@ <string name="kind_outbox_relays">Outbox relayer</string> <string name="kind_pay_to">Betala till</string> <string name="kind_people_lists">Personlistor</string> + <string name="kind_picture_curation_set">Bildlista</string> <string name="kind_pictures">Bilder</string> <string name="kind_pins">Nålar</string> <string name="kind_podcast_episode">Poddavsnitt</string> @@ -3728,6 +4010,7 @@ <string name="kind_search_relays">Sökreläer</string> <string name="kind_shorts">Shorts</string> <string name="kind_shorts_repl">Shorts (svar)</string> + <string name="kind_simple_groups">Grupper</string> <string name="kind_torrent_comments">Torrentkommentarer</string> <string name="kind_torrents">Torrenter</string> <string name="kind_trusted_providers">Betrodda leverantörer</string> @@ -3736,7 +4019,10 @@ <string name="kind_user_settings">Användarinställningar</string> <string name="kind_user_status">Användarstatus</string> <string name="kind_video">Video</string> + <string name="kind_video_collaboration">Videosamarbete</string> + <string name="kind_video_list">Videolista</string> <string name="kind_video_repl">Video (svar)</string> + <string name="kind_video_subtitles">Videoundertexter</string> <string name="kind_voice_msg">Röstmeddelande</string> <string name="kind_voice_reply">Röstsvar</string> <string name="kind_wake">Väckning</string> @@ -3773,6 +4059,8 @@ <string name="location_change_place">Ändra plats på kartan</string> <string name="location_channels">Platskanaler</string> <string name="location_pick_on_map">Välj en plats på kartan</string> + <string name="login_bunker_not_supported">Amethyst för Android kan inte logga in med en bunker://-adress. Den delar ut sådana så att andra appar kan be den signera, men den kan inte logga in med en. Använd en extern signatärapp eller Amethyst Desktop.</string> + <string name="login_nostrconnect_not_supported">Det där är en app som vill ansluta till din signatär, inte ett sätt att logga in. Logga in först och öppna den sedan från NIP-46-signatärskärmen.</string> <string name="login_with_a_private_key_to_be_able_to_boost_posts">Logga in med en privat nyckel för att kunna boosta inlägg</string> <string name="login_with_a_private_key_to_be_able_to_follow">Logga in med en privat nyckel för att kunna följa</string> <string name="login_with_a_private_key_to_be_able_to_hide_word">Du använder en offentlig nyckel, och offentliga nycklar är endast läsbara. Logga in med en privat nyckel för att kunna dölja ett ord eller en mening</string> @@ -3863,6 +4151,7 @@ <string name="mute_notifications">Tysta aviseringar</string> <string name="muted_button">Ljud avstängt. Klicka för att ta bort ljudlöst</string> <string name="my_blossom_data">Mina Blossom-filer</string> + <string name="my_fitness">Min träning</string> <string name="my_lists">Mina listor</string> <string name="name_is_required">Namn är obligatoriskt</string> <string name="namecoin_response_time">%1$dms</string> @@ -4126,6 +4415,7 @@ </plurals> <string name="nip46_signer_remote_app">Fjärrsignatärapp</string> <string name="nip46_signer_title">Fjärrsignatär</string> + <string name="nip51_list_has_private_members">Den här listan har privata medlemmar som bara dess skapare kan se</string> <plurals name="nip82_assets_count"> <item quantity="one">%1$d tillgång paketerad</item> <item quantity="other">%1$d tillgångar buntade</item> @@ -4656,12 +4946,21 @@ <string name="route_calendars">Kalendrar</string> <string name="route_chess">Schack</string> <string name="route_discover">Upptäck</string> + <string name="route_edit_geocache">Redigera cache</string> + <string name="route_edit_geocache_hunt">Redigera jakt</string> + <string name="route_geocache_detail">Cache</string> + <string name="route_geocache_hunt_detail">Jakt</string> + <string name="route_geocache_hunts">Cachejakter</string> + <string name="route_geocaches">Geocacher</string> <string name="route_home">Hem</string> <string name="route_import_follows">Importera följare</string> + <string name="route_log_geocache_find">Logga ett fynd</string> <string name="route_media">Media</string> <string name="route_messages">Meddelanden</string> <string name="route_music_playlists">Spellistor</string> <string name="route_music_tracks">Musik</string> + <string name="route_new_geocache">Göm en cache</string> + <string name="route_new_geocache_hunt">Ny jakt</string> <string name="route_notifications">Aviseringar</string> <string name="route_podcast_episodes">Avsnitt</string> <string name="route_podcasts">Poddar</string> @@ -4851,6 +5150,18 @@ <string name="vanish_request_sent">Begäran om försvinnande skickad</string> <string name="video_codec_h265_description">Bättre kvalitet med mindre filstorlek, men inte alla enheter stöder H.265-uppspelning.</string> <string name="video_codec_h265_label">Använd H.265/HEVC-codec</string> + <string name="video_collaboration_accepted_on">Accepterade ett samarbetsomnämnande</string> + <string name="video_collaboration_declined_on">Avböjde ett samarbetsomnämnande</string> + <string name="video_credit_accepted">Omnämnande accepterat</string> + <string name="video_credit_audio_from">Ljud från</string> + <string name="video_credit_featuring">Med</string> + <string name="video_credit_inspired_by">Inspirerad av</string> + <string name="video_credit_references">Referenser</string> + <string name="video_curation_set_private_items">Privat lista</string> + <plurals name="video_curation_set_video_count"> + <item quantity="one">%1$d video</item> + <item quantity="other">%1$d videor</item> + </plurals> <string name="video_download_has_started_toast">Nedladdning av video har startat…</string> <string name="video_player_settings">Videospelarens knappar</string> <string name="video_saved_to_the_gallery">Videon sparad i telefonens videogalleri</string> @@ -4910,165 +5221,18 @@ <string name="the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct">Mottagarens Lightning-tjänst på %1$s är inte tillgänglig. Den beräknades från Lightning Address "%2$s". Fel: %3$s. Kontrollera om servern är uppe och om Lightning Addressen är korrekt</string> <string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user">Det gick inte att skapa en blixtfaktura från %1$s: Elementet pr hittades inte i den resulterande JSON.</string> <string name="unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user">Det gick inte att skapa en blixt faktura. Meddelande från %1$s: %2$s</string> + <string name="marmot_invite_device_success">Den här enheten tar nu emot dina Marmot-inbjudningar.</string> + <string name="marmot_invite_device_failure">Det gick inte att publicera KeyPackage: %1$s</string> + <string name="marmot_invite_device_rejected">Inget relä accepterade KeyPackage. Kontrollera dina reläinställningar och försök igen.</string> + <string name="chat_delivery_sending">Skickar…</string> + <string name="chat_delivery_failed">Kunde inte skickas. Tryck för att försöka igen.</string> + <plurals name="marmot_unread_messages"> + <item quantity="one">%1$d nytt meddelande</item> + <item quantity="other">%1$d nya meddelanden</item> + </plurals> <!-- cordn: groups delivered by an MCP coordinator rather than by relays. The exposure strings state what the coordinator operator learns; see spec/00.md §8 and quartz/plans/2026-09-17-cordn-interop.md. --> - <!-- cordn entries in the settings catalog and the Messages chat-type - list. Here rather than in amethyst/res because both screens read - them through Res. --> - <!-- cordn strings on screens main migrated to Res: the shared inbox row - and the new-conversation picker. --> - <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a - one-shot handoff rather than continuous sync. The exposure strings say - what leaves the device; the fork strings say why the old phone stops. --> - <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> - <string name="geocache_unnamed">Geocache</string> - <string name="geocache_type_traditional">Traditionell</string> - <string name="geocache_type_multi">Multicache</string> - <string name="geocache_type_mystery">Mystery</string> - <string name="geocache_size_micro">Mikro</string> - <string name="geocache_size_small">Liten</string> - <string name="geocache_size_regular">Normal</string> - <string name="geocache_size_large">Stor</string> - <string name="geocache_size_other">Annan storlek</string> - <string name="geocache_difficulty_short">D%1$d</string> - <string name="geocache_terrain_short">T%1$d</string> - <string name="geocache_first_to_find">Först att hitta (FTF)</string> - <string name="geocache_is_art">Konstverk</string> - <string name="geocache_mission">Key Quest</string> - <string name="geocache_needs_verification">Verifierade fynd</string> - <string name="geocache_claimed">Tagen</string> - <string name="geocache_archived">Arkiverad</string> - <string name="geocache_hint_tap_to_reveal">Kört fast? Tryck för att visa ledtråden</string> - <string name="geocache_hint_other_reading">Går den inte att läsa? Tryck för den andra versionen</string> - <string name="geocache_nearby">I närheten</string> - <string name="geocache_found_it">Hittad!</string> - <string name="geocache_verified_find">Verifierat fynd</string> - <string name="geocache_invalid_proof">Beviset stämmer inte</string> - <string name="geocache_loading">Läser in cache…</string> - <string name="geocache_hidden_by">Gömd av %1$s</string> - <string name="geocache_navigate">Navigera</string> - <string name="geocache_copy_coordinates">Kopiera koordinater</string> - <string name="geocache_coordinates_copied">Koordinater kopierade</string> - <string name="geocache_photos_section">Foton</string> - <string name="geocache_mission_section">Key Quest-uppdrag</string> - <string name="geocache_logs_section">Loggar</string> - <string name="geocache_no_logs">Ingen har loggat den här cachen ännu. Bli först.</string> - <string name="geocache_i_found_it">Jag hittade den</string> - <string name="geocache_didnt_find_it">Hittade den inte</string> - <string name="geocache_add_note">Lägg till en anteckning</string> - <string name="geocache_needs_maintenance">Rapportera behov av underhåll</string> - <string name="geocache_add_to_hunt">Lägg till i en jakt</string> - <string name="geocache_hunt_new">Ny jakt…</string> - <string name="geocache_hunt_none_yet">Du har inte skapat någon jakt ännu.</string> - <string name="geocache_hunt_already_on">Redan med i den här jakten</string> - <plurals name="geocache_caches_here"> - <item quantity="one">%1$d cache här</item> - <item quantity="other">%1$d cacher här</item> - </plurals> - <string name="geocache_photo_remove">Ta bort det här fotot</string> - <string name="geocache_archived_notice">Den här cachen har arkiverats. Den är inte längre i spel.</string> - <string name="geocache_ftf_won_by">FTF tagen av %1$s</string> - <string name="geocache_ftf_available">FTF är fortfarande ledig</string> - <string name="geocache_owner_actions">Ägare</string> - <string name="geocache_owner_edit">Redigera den här cachen</string> - <string name="geocache_owner_archive">Arkivera den här cachen</string> - <string name="geocache_owner_archive_confirm">Arkivera den. Hittare ser att den är ur spel, och beskrivningen går fortfarande att läsa.</string> - <string name="geocache_owner_lock_ftf">Lås den första hittaren</string> - <string name="geocache_owner_lock_ftf_explain">Publicerar en F-tagg som anger %1$s. Detta är permanent.</string> - <string name="geocache_owner_no_finders">Inga verifierade fynd ännu, så det finns ingen att låsa.</string> - <string name="geocache_owner_show_qr">Visa verifieringskoden</string> - <string name="geocache_owner_qr_warning">Skriv ut den och lägg den i cachen. Amethyst sparar den inte — om du tappar bort den kan hittare inte längre bevisa att de var där.</string> - <string name="geocache_log_type_dnf">Hittade inte (DNF)</string> - <string name="geocache_log_type_note">Anteckning</string> - <string name="geocache_log_type_maintenance">Behöver underhåll</string> - <string name="geocache_log_title">Logga ett fynd</string> - <string name="geocache_log_cache_label">Cache</string> - <string name="geocache_log_your_log">Din logg</string> - <string name="geocache_log_placeholder">Hur gick det?</string> - <string name="geocache_log_add_photo">Lägg till ett foto</string> - <string name="geocache_log_mission_answer">Ditt Key Quest-svar</string> - <string name="geocache_log_proof_section">Bevis på närvaro</string> - <string name="geocache_log_scan_code">Skanna koden i cachen</string> - <string name="geocache_log_scan_again">Skanna en annan kod</string> - <string name="geocache_log_proof_verified">Verifierat — du var vid cachen</string> - <string name="geocache_log_proof_explain">Signeras en gång med koden i lådan. Nyckeln sparas aldrig och lämnar aldrig den här skärmen.</string> - <string name="geocache_log_proof_bad_code">Det där är ingen verifieringskod.</string> - <string name="geocache_log_proof_wrong_cache">Den koden hör till en annan cache.</string> - <string name="geocache_log_proof_optional">Den här cachen kräver inget bevis, så du kan logga den direkt.</string> - <string name="geocache_log_no_location_attached">Din egen plats bifogas inte till den här loggen.</string> - <string name="geocache_log_post">Publicera</string> - <string name="geocache_log_comment_title">Lägg till en logg</string> - <string name="geocache_tab_nearby">I närheten</string> - <string name="geocache_tab_map">Karta</string> - <string name="geocache_tab_hunts">Jakter</string> - <string name="geocache_tab_finds">Fynd</string> - <string name="geocache_tab_mine">Mina</string> - <string name="geocache_filter_unfound">Ej hittade</string> - <string name="geocache_filter_difficulty">D ≤ %1$d</string> - <string name="geocache_filter_terrain">T ≤ %1$d</string> - <string name="geocache_filter_has_prize">Har ett pris</string> - <string name="geocache_filter_verified_only">Bevis krävs</string> - <string name="geocache_use_my_location">Använd min plats</string> - <string name="geocache_sorted_by_distance">Sorterat efter avstånd</string> - <string name="geocache_sorted_by_recent">Nyaste först</string> - <string name="geocache_hide_a_cache">Göm en cache</string> - <string name="geocache_empty_nearby">Inga cacher här ännu. Prova en bredare följlista, eller göm den första.</string> - <string name="geocache_empty_finds">Du har inte loggat något fynd ännu.</string> - <string name="geocache_empty_mine">Du har inte gömt någon cache ännu.</string> - <string name="geocache_empty_hunts">Inga jakter ännu.</string> - <string name="geocache_stat_finds">Fynd</string> - <string name="geocache_stat_ftf">FTF</string> - <string name="geocache_stat_verified">Verifierade</string> - <string name="geocache_stat_hidden">Gömda</string> - <string name="geocache_map_recenter">Centrera på mig</string> - <string name="geocache_map_long_press">Tryck länge på kartan för att gömma en cache där</string> - <string name="geocache_map_open">Öppna</string> - <string name="geocache_new_section_where">Var</string> - <string name="geocache_new_section_what">Vad</string> - <string name="geocache_new_section_extras">Extra</string> - <string name="geocache_new_section_proof">Bevis</string> - <string name="geocache_new_section_relays">Reläer</string> - <string name="geocache_new_location_warning">Detta publicerar en offentlig plats med cirka 5 meters noggrannhet. Alla på Nostr kan se den, och en publicerad händelse kan inte återkallas.</string> - <string name="geocache_new_pick_location">Välj platsen</string> - <string name="geocache_new_ladder">Publicerar %1$s, plus grövre celler ned till 3 tecken för närhetssökning.</string> - <string name="geocache_new_name">Namn</string> - <string name="geocache_new_description">Beskrivning</string> - <string name="geocache_new_type">Typ</string> - <string name="geocache_new_size">Storlek</string> - <string name="geocache_new_difficulty">Svårighetsgrad</string> - <string name="geocache_new_terrain">Terräng</string> - <string name="geocache_new_hint">Ledtråd</string> - <string name="geocache_new_hint_preview">Hittare kommer att se</string> - <string name="geocache_new_hint_scramble">Kryptera ledtråden (ROT13)</string> - <string name="geocache_new_mission">Key Quest-uppdrag</string> - <string name="geocache_new_modifiers">Tillägg</string> - <string name="geocache_new_require_proof">Kräv bevis på närvaro</string> - <string name="geocache_new_proof_ready">En verifieringsnyckel har skapats för den här cachen.</string> - <string name="geocache_new_log_relays">Var hittare ska logga</string> - <string name="geocache_new_publish">Publicera</string> - <string name="geocache_new_needs_name">Ge cachen ett namn.</string> - <string name="geocache_new_needs_location">Välj var cachen är gömd.</string> - <string name="geocache_hunt_progress">%1$d av %2$d hittade</string> - <string name="geocache_hunt_start">Starta jakten</string> - <plurals name="geocache_hunt_caches"> - <item quantity="one">%1$d cache</item> - <item quantity="other">%1$d cacher</item> - </plurals> - <string name="geocache_hunt_title">Titel</string> - <string name="geocache_hunt_description">Beskrivning</string> - <string name="geocache_hunt_banner">Bannerbild</string> - <string name="geocache_hunt_pick_caches">Cacher, i ordning</string> - <string name="geocache_hunt_add_cache">Lägg till en cache</string> - <string name="geocache_hunt_move_up">Flytta upp</string> - <string name="geocache_hunt_move_down">Flytta ned</string> - <string name="geocache_hunt_remove">Ta bort</string> - <string name="geocache_hunt_theme">Sidtema</string> - <string name="geocache_hunt_map_style">Kartstil</string> - <string name="geocache_hunt_needs_title">Ge jakten en titel.</string> - <string name="geocache_hunt_needs_caches">Lägg till minst en cache.</string> - <string name="geocache_publish_failed">Kunde inte publicera. Kontrollera reläanslutningen och försök igen.</string> - <string name="cordn_groups_title">Cordn-grupper</string> <string name="cordn_badge_coordinator">Koordinatorgrupp</string> <string name="cordn_badge_coordinator_desc">Levereras av en koordinator, inte av reläer</string> <string name="cordn_exposure_title">Vad den här koordinatorn kan se</string> @@ -5106,6 +5270,9 @@ <string name="cordn_link_relays">Nås via</string> <string name="cordn_link_no_coordinator">Den här länken anger ingen koordinator, så den kan inte följas på egen hand. Fråga den som skickade den vilken koordinator som för vidare gruppen.</string> <string name="cordn_link_not_joinable">Att granska en länk går inte med i något, och att ha en gör dig inte till medlem.</string> + <!-- cordn entries in the settings catalog and the Messages chat-type + list. Here rather than in amethyst/res because both screens read + them through Res. --> <string name="cordn_link_title">Cordn-grupplänk</string> <string name="cordn_link_search_keywords">cordn koordinator grupp länk inbjudan mls chatt metadata integritet exponering coordinator group link invite</string> <string name="cordn_coordinators_title">Cordn-koordinatorer</string> @@ -5114,6 +5281,10 @@ <string name="cordn_keypackages_search_keywords">cordn nyckelpaket publicera inbjudan reserv koordinator mls key package last resort</string> <string name="cordn_backup_title">Cordn-säkerhetskopia</string> <string name="cordn_backup_search_keywords">cordn säkerhetskopia återställ exportera importera lösenfras grupper återställning backup restore</string> + <string name="chat_type_cordn_title">Cordn-grupper</string> + <string name="chat_type_cordn_desc">End-to-end-krypterade grupper som ordnas av en koordinator du väljer, i stället för av reläer.</string> + <!-- cordn strings on screens main migrated to Res: the shared inbox row + and the new-conversation picker. --> <string name="cordn_group_untitled">Grupp %1$s</string> <string name="cordn_group_no_messages_yet">Inga meddelanden ännu</string> <string name="cordn_group_via_coordinator">Cordn-grupp, förs vidare av %1$s</string> @@ -5122,8 +5293,19 @@ <string name="cordn_preview_photo">Foto</string> <string name="cordn_preview_video">Video</string> <string name="cordn_preview_file">Fil</string> + <string name="new_conversation_cordn_title">Cordn-grupp</string> + <string name="new_conversation_cordn_tagline">Krypterad gruppchatt som ordnas av en koordinator du väljer.</string> + <string name="new_conversation_cordn_chip">Koordinerad</string> + <string name="new_conversation_cordn_best">Bäst för team som vill ha en pålitlig ordning i konversationen.</string> + <string name="new_conversation_cordn_cta">Skapa Cordn-grupp</string> + <string name="new_conversation_cordn_pro_1">End-to-end-krypterad med MLS — koordinatorn kan aldrig läsa ett meddelande.</string> + <string name="new_conversation_cordn_pro_2">En gemensam ordning för alla, så att historiken inte kan kastas om av en felaktig klocka.</string> + <string name="new_conversation_cordn_con_1">Koordinatorn får veta vilka som är med i gruppen och när ni pratar, även om den inte kan läsa vad ni säger.</string> <string name="cordn_invitations_entry">Inbjuden till en cordn-grupp?</string> <string name="cordn_invitations_entry_action">Granska inbjudningar</string> + <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a + one-shot handoff rather than continuous sync. The exposure strings say + what leaves the device; the fork strings say why the old phone stops. --> <string name="cordn_hub_title">Cordn</string> <string name="cordn_hub_explainer">Grupper som levereras av en koordinator du väljer, i stället för av reläer.</string> <string name="cordn_hub_link">Granska en grupplänk</string> @@ -5173,453 +5355,6 @@ <string name="cordn_migrate_no_server">Ställ in en mediaserver först — de krypterade dokumenten behöver någonstans att ligga medan den andra telefonen hämtar dem.</string> <string name="cordn_migrate_exposure_title">Vad som lämnar den här enheten</string> <string name="cordn_migrate_exposure_body">Ditt grupptillstånd krypteras och laddas upp till din mediaserver så att den andra telefonen kan hämta det. Det är oläsligt för servern, som bara ser en storlek och en tidpunkt. Radera blobbarna efteråt om du inte vill lämna något kvar.</string> - <string name="cordn_action_delete">Radera</string> - <string name="cordn_action_edit">Redigera</string> - <string name="cordn_action_editing">Redigerar ditt meddelande</string> - <string name="cordn_action_go_to_message">Gå till meddelandet</string> - <string name="cordn_action_pin">Fäst</string> - <string name="cordn_action_reply">Svara</string> - <string name="cordn_action_unpin">Lossa</string> - <string name="cordn_admin_action_failed">Ändringen kunde inte genomföras.</string> - <string name="cordn_admin_coordinator_silent">Koordinatorn svarade inte, så inget ändrades. Försök igen om en stund.</string> - <string name="cordn_admin_no_key_package">%1$s har inte publicerat något nyckelpaket till den här koordinatorn, så de kan inte läggas till ännu. Be dem att först öppna cordn på den här koordinatorn.</string> - <string name="cordn_admin_no_self_remove">Du kan inte ta bort dig själv från en cordn-grupp.</string> - <string name="cordn_admin_no_welcome">%1$s kunde inte ges någon väg in i gruppen, så tillägget avbröts.</string> - <string name="cordn_admin_not_a_member">%1$s är inte med i den här gruppen.</string> - <string name="cordn_admin_someone">Den personen</string> - <string name="cordn_admin_wrong_key_package">Den här koordinatorn lämnade ut ett nyckelpaket som tillhör någon annan, så %1$s lades inte till. Inget ändrades.</string> - <string name="cordn_backup_contents_body">Dina gruppkrypteringsnycklar, dina nyckelpaket och varje meddelande i de grupperna. Den som öppnar filen kan läsa konversationerna. Lösenfrasen är det enda som skyddar den när den väl lämnat Amethyst.</string> - <string name="cordn_backup_contents_title">Vad filen innehåller</string> - <string name="cordn_backup_explainer">Din nsec kan inte återskapa cordn-grupper. En cordn-grupp lever som krypteringstillstånd på den här enheten och som en ordnad ström hos en koordinator som inte kan läsa den — förlorar du enheten utan säkerhetskopia är gruppen borta, inklusive allt som redan sagts i den.</string> - <string name="cordn_backup_export">Exportera</string> - <string name="cordn_backup_exported">Säkerhetskopian sparad.</string> - <string name="cordn_backup_failed">Det fungerade inte. Kontrollera lösenfrasen och filen.</string> - <string name="cordn_backup_passphrase">Lösenfras</string> - <string name="cordn_backup_restore">Återställ</string> - <string name="cordn_backup_restore_body">Återställning ersätter cordn-grupperna på den här enheten med dem i filen. Den är till för en telefon som tagit över från en annan — inte ett sätt att använda samma grupper på två enheter samtidigt.</string> - <string name="cordn_backup_restore_confirm_body">Alla cordn-grupper som nu finns på den här enheten tas bort och ersätts med dem i filen.\n\nÅterställ inte till en andra telefon medan den första fortfarande används. Båda skulle ha samma gruppnycklar, och så snart båda har skickat ett meddelande går gruppen sönder för alla — tyst, och utan väg tillbaka.</string> - <string name="cordn_backup_restore_confirm_title">Ersätta den här enhetens cordn-grupper?</string> - <string name="cordn_backup_restore_title">Återställ</string> - <string name="cordn_backup_restored">Återställd.</string> - <string name="cordn_chat_empty_description">Allt som skickas här krypteras för den här gruppen. Koordinatorn förmedlar det utan att kunna läsa det.</string> - <string name="cordn_chat_empty_title">Inga meddelanden ännu</string> - <string name="cordn_chat_unread_divider">Nya meddelanden</string> - <string name="cordn_chat_yesterday">I går</string> - <string name="cordn_composer_hint">Meddelande</string> - <string name="cordn_coordinators_add_action">Lägg till</string> - <string name="cordn_coordinators_add_failed">Kunde inte öppna den koordinatorn.</string> - <string name="cordn_coordinators_copy_key">Kopiera koordinatorns npub</string> - <string name="cordn_coordinators_discover_action">Leta efter koordinatorer</string> - <string name="cordn_coordinators_discover_add">Lägg till</string> - <string name="cordn_coordinators_discover_explainer">Läser annonser som dina reläer redan har. Ingen koordinator kontaktas, så ingen av dem får veta att du letade.</string> - <string name="cordn_coordinators_discover_failed">Kunde inte läsa annonser</string> - <string name="cordn_coordinators_discover_none">Ingen annonserar på dina reläer.</string> - <string name="cordn_coordinators_discover_seen">Annonserade för %1$s sedan</string> - <string name="cordn_coordinators_discover_seen_on">Senast annonserad %1$s</string> - <plurals name="cordn_coordinators_discover_unheard"> - <item quantity="one">Inget hittades, och %1$d av dina reläer svarade inte.</item> - <item quantity="other">Inget hittades, och %1$d av dina reläer svarade inte.</item> - </plurals> - <string name="cordn_coordinators_explainer">En koordinator ordnar en grupps meddelanden och är det enda som kan göra det. Förlorar man den förloras ordningen, och en andra koordinator speglar inte den första — så det här är namngivna saker du väljer, inte utbytbara reläer.</string> - <plurals name="cordn_coordinators_health_down"> - <item quantity="one">Svarar inte — %1$d anrop i rad har misslyckats. Grupper på den synkroniseras inte.</item> - <item quantity="other">Svarar inte — %1$d anrop i rad har misslyckats. Grupper på den synkroniseras inte.</item> - </plurals> - <string name="cordn_coordinators_health_ok">Svarar.</string> - <string name="cordn_coordinators_health_retrying">Ett anrop misslyckades; försöker igen.</string> - <string name="cordn_coordinators_health_unknown">Inget har efterfrågats ännu.</string> - <string name="cordn_coordinators_hide_older">Dölj dem som slutat annonsera</string> - <string name="cordn_coordinators_identify">Fråga vem den är</string> - <string name="cordn_coordinators_key">Koordinatornyckel</string> - <string name="cordn_coordinators_label">Ett namn för den (ditt eget, valfritt)</string> - <string name="cordn_coordinators_label_note">Det här namnet är ditt och stannar på den här enheten. En koordinator kan inte bevisa ett namn, så den ombeds aldrig uppge något.</string> - <string name="cordn_coordinators_more">Fler åtgärder</string> - <string name="cordn_coordinators_none">Inga koordinatorer ännu.</string> - <string name="cordn_coordinators_purge">Rensa bort</string> - <string name="cordn_coordinators_purge_body">Att ta bort behåller grupperna på den här enheten, så om du lägger till koordinatorn igen kommer de tillbaka. Att rensa bort raderar dem: gruppnycklarna, lässtatusen och dina nyckelpaket försvinner, och de grupperna kan bara nås igen via en ny inbjudan.\n\nKoordinatorn meddelas inte och behåller allt den redan hade.</string> - <string name="cordn_coordinators_purge_title">Rensa bort den här koordinatorn?</string> - <string name="cordn_coordinators_relays">%1$s</string> - <string name="cordn_coordinators_relays_label">Svarar på</string> - <string name="cordn_coordinators_relays_more">%1$s +%2$d till</string> - <string name="cordn_coordinators_remove">Ta bort</string> - <string name="cordn_coordinators_rename">Byt namn</string> - <string name="cordn_coordinators_server">Säger sig vara: %1$s</string> - <string name="cordn_coordinators_server_claim">En koordinators namn och version är strängar den själv valt. Dess publika nyckel är det enda som identifierar den.</string> - <string name="cordn_coordinators_server_silent">Svarade, men angav inget namn.</string> - <string name="cordn_coordinators_show_all">Visa alla %1$d</string> - <string name="cordn_coordinators_show_fewer">Visa färre</string> - <plurals name="cordn_coordinators_show_older"> - <item quantity="one">Visa %1$d som slutat annonsera</item> - <item quantity="other">Visa %1$d som slutat annonsera</item> - </plurals> - <string name="cordn_coordinators_stale_note">Dessa har inte annonserat på över en månad. Att skapa en grupp på en som försvunnit kommer att misslyckas.</string> - <string name="cordn_create_action">Skapa grupp</string> - <plurals name="cordn_create_action_invite"> - <item quantity="one">Skapa och bjud in %1$d person</item> - <item quantity="other">Skapa och bjud in %1$d personer</item> - </plurals> - <string name="cordn_create_add_member">Lägg till någon</string> - <string name="cordn_create_admin">Admin</string> - <plurals name="cordn_create_admin_count"> - <item quantity="one">%1$d person kan lägga till och ta bort</item> - <item quantity="other">%1$d personer kan lägga till och ta bort</item> - </plurals> - <string name="cordn_create_admin_only_me">Bara jag hanterar den här gruppen</string> - <string name="cordn_create_admin_only_me_on">Bara du kan bjuda in, ta bort medlemmar eller ändra gruppens uppgifter. Du kan lämna över det till någon annan senare.</string> - <string name="cordn_create_coordinator">Koordinator</string> - <string name="cordn_create_coordinator_change">Ändra</string> - <string name="cordn_create_coordinator_discover">Hitta koordinatorer</string> - <string name="cordn_create_coordinator_hide">Klar</string> - <string name="cordn_create_coordinator_new">En annan koordinator</string> - <string name="cordn_create_coordinator_pubkey">Koordinatorns publika nyckel (hex eller npub)</string> - <string name="cordn_create_coordinator_relays">Reläer den svarar på, ett per rad</string> - <plurals name="cordn_create_coverage_all"> - <item quantity="one">Når %1$d person — alla du valt</item> - <item quantity="other">Når alla dina %1$d personer</item> - </plurals> - <string name="cordn_create_coverage_partial">Når %1$d av %2$d — resten skulle bli utanför</string> - <string name="cordn_create_coverage_unknown">Inte kontrollerad — den tillfrågas om du väljer den</string> - <string name="cordn_create_description">Beskrivning (valfritt)</string> - <string name="cordn_create_egalitarian">Alla kan lägga till och ta bort</string> - <string name="cordn_create_egalitarian_note">Alla i gruppen kan bjuda in, ta bort medlemmar och ändra gruppens uppgifter. Vilken medlem som helst kan utse admins senare, vilket upphäver detta.</string> - <string name="cordn_create_explainer">En cordn-grupp ordnas av en koordinator du väljer. Gruppen skapas på den här enheten — koordinatorn får veta att gruppen finns först när du bjuder in någon eller skickar ett meddelande.</string> - <string name="cordn_create_failed">Gruppen kunde inte skapas.</string> - <string name="cordn_create_name">Gruppnamn</string> - <string name="cordn_create_no_coordinator">Ingen koordinator vald</string> - <string name="cordn_create_outcome_explainer">Varje inbjudan är ett Welcome-meddelande som lämnas hos koordinatorn, så de kan misslyckas en i taget. Ingen har meddelats: de ser det nästa gång de öppnar sin app.</string> - <string name="cordn_create_outcome_failed">Koordinatorn avvisade inbjudan</string> - <string name="cordn_create_outcome_link">Skicka en länk</string> - <string name="cordn_create_outcome_no_welcome">Tillagd i gruppen, men inbjudan nådde inte fram — försök igen från gruppskärmen</string> - <string name="cordn_create_outcome_open">Öppna chatten</string> - <string name="cordn_create_outcome_skipped">Ingen nyckel här, så inget Welcome-meddelande kunde lämnas</string> - <string name="cordn_create_outcome_title">Gruppen är skapad</string> - <string name="cordn_create_outcome_waiting">Väntar på dem</string> - <string name="cordn_create_people_done">Klar</string> - <string name="cordn_create_people_empty">Ingen tillagd ännu. En grupp kan skapas tom och inbjudningar skickas senare, men att ange personer nu är det som visar vilken koordinator som kan nå dem.</string> - <string name="cordn_create_people_none">Ingen ännu — tryck för att lägga till personer</string> - <string name="cordn_create_people_note">Att ange personer nu är det som talar om för oss vilken koordinator som kan nå dem. Var och en får ett Welcome-meddelande som lämnas hos den koordinatorn, och som de hämtar nästa gång de öppnar sin app.</string> - <plurals name="cordn_create_reach_count"> - <item quantity="one">Nåbar via %1$d koordinator</item> - <item quantity="other">Nåbar via %1$d koordinatorer</item> - </plurals> - <string name="cordn_create_reach_none">Ingen nyckel hos någon koordinator du använder</string> - <string name="cordn_create_reach_unknown">Inte kontrollerad ännu</string> - <string name="cordn_create_step_admins">Vem som kan lägga till och ta bort</string> - <string name="cordn_create_step_name">Namnge den</string> - <string name="cordn_create_step_people">Vilka som är med</string> - <string name="cordn_create_step_where">Var den finns</string> - <string name="cordn_create_title">Ny Cordn-grupp</string> - <plurals name="cordn_create_unreachable_count"> - <item quantity="one">%1$d person kan inte nås av någon koordinator du använder</item> - <item quantity="other">%1$d personer kan inte nås av någon koordinator du använder</item> - </plurals> - <string name="cordn_create_you">Du, alltid admin</string> - <string name="cordn_delete_confirm_body">Alla i gruppen kommer att se meddelandet som återkallat, och Amethyst slutar visa texten. Koordinatorn har fortfarande den krypterade kopia som skickades, och den som redan har läst det har redan läst det.</string> - <string name="cordn_delete_confirm_title">Återkalla meddelande</string> - <string name="cordn_delivery_accepted">Accepterat av koordinatorn</string> - <string name="cordn_exposure_close">Uppfattat</string> - <string name="cordn_exposure_open">Vad den här koordinatorn kan se</string> - <string name="cordn_group_info">Gruppinfo</string> - <string name="cordn_group_unavailable">Den här gruppen är inte tillgänglig. Dess koordinator kan ha tagits bort.</string> - <string name="cordn_group_unavailable_detail">Lägg till en koordinator i Inställningar för att börja använda cordn-grupper.</string> - <plurals name="cordn_groups_count"> - <item quantity="one">%1$d grupp</item> - <item quantity="other">%1$d grupper</item> - </plurals> - <string name="cordn_groups_down_note">Inget kommer fram från de här grupperna. Amethyst fortsätter att försöka.</string> - <string name="cordn_groups_manage">Koordinatorer</string> - <string name="cordn_groups_none">Inga cordn-grupper ännu</string> - <string name="cordn_groups_none_detail">En cordn-grupp finns hos en koordinator som håller reda på medlemskapet och skickar meddelanden vidare, utan att någonsin se vad någon skriver.</string> - <string name="cordn_groups_start">Starta en</string> - <string name="cordn_info_add_member">Lägg till någon</string> - <string name="cordn_info_add_member_none">Ingen hittades med det namnet.</string> - <string name="cordn_info_add_member_note">De kan bara läggas till om de har publicerat ett nyckelpaket till den här koordinatorn.</string> - <string name="cordn_info_add_member_placeholder">Namn, npub eller namn@domän</string> - <string name="cordn_info_admin_badge">Admin</string> - <plurals name="cordn_info_admins_kept"> - <item quantity="one">Gruppens %1$d admin förblir oförändrad.</item> - <item quantity="other">Gruppens %1$d admins förblir oförändrade.</item> - </plurals> - <string name="cordn_info_coordinator">Koordinator</string> - <string name="cordn_info_coordinator_key">Koordinatornyckel</string> - <string name="cordn_info_coordinator_nprofile">Koordinatorlänk</string> - <string name="cordn_info_copy_nprofile">Kopiera koordinatorns nprofile</string> - <string name="cordn_info_edit_details">Redigera uppgifter</string> - <string name="cordn_info_egalitarian">Den här gruppen har inga admins: alla kan lägga till, ta bort och byta namn, permanent.</string> - <string name="cordn_info_epoch">Epok</string> - <string name="cordn_info_gid">Grupp-id</string> - <string name="cordn_info_has_key_package">Kan läggas till via den här koordinatorn</string> - <string name="cordn_info_members">Medlemmar</string> - <string name="cordn_info_remove_confirm_body">%1$s slutar ta emot meddelanden härifrån. De behåller det de redan har läst, och detta kan inte ångras — de skulle behöva bjudas in igen.</string> - <string name="cordn_info_remove_confirm_title">Ta bort från gruppen?</string> - <string name="cordn_info_remove_member">Ta bort</string> - <string name="cordn_info_save">Spara</string> - <string name="cordn_info_technical">Tekniska detaljer</string> - <string name="cordn_invitations_accept">Gå med</string> - <string name="cordn_invitations_decline">Avböj</string> - <string name="cordn_invitations_decline_warning">Att avböja är permanent. För att komma in igen måste du bjudas in på nytt.</string> - <string name="cordn_invitations_explainer">Kontrolleras när du öppnar den här skärmen, och aldrig annars — att fråga en koordinator om någon har bjudit in dig avslöjar att du finns här, så Amethyst gör det inte i bakgrunden.</string> - <string name="cordn_invitations_load_failed">Kunde inte läsa inbjudningar.</string> - <plurals name="cordn_invitations_members"> - <item quantity="one">%1$d person är redan med i den här gruppen</item> - <item quantity="other">%1$d personer är redan med i den här gruppen</item> - </plurals> - <string name="cordn_invitations_members_more">+%1$d till</string> - <string name="cordn_invitations_no_coordinators">Du har ingen koordinator öppen. cordn-inbjudningar kommer via en koordinator, så det finns ingenstans att leta ännu.</string> - <string name="cordn_invitations_none">Inga väntande inbjudningar.</string> - <string name="cordn_invitations_refresh">Kontrollera igen</string> - <string name="cordn_invitations_skipped">En inbjudan lämnades till en annan enhet</string> - <string name="cordn_invitations_title">Cordn-inbjudningar</string> - <string name="cordn_invitations_unreachable">%1$s svarade inte</string> - <string name="cordn_invitations_via">Via %1$s</string> - <string name="cordn_keypackages_disclosure">Publicering signerar ett belägg med din egen kontonyckel. Den koordinatorn vet då att kontot finns och går att bjuda in — permanent, och oavsett om någon bjuder in dig.</string> - <string name="cordn_keypackages_explainer">Andra kan bara lägga till dig i en cordn-grupp genom att ta ett nyckelpaket du publicerat till den koordinatorn. cordn har ingen annan plats att förvara ett, så utan något publicerat kan ingen bjuda in dig — och inget talar om för dem varför.</string> - <string name="cordn_keypackages_failed">Kunde inte läsa nyckelpaketen.</string> - <string name="cordn_keypackages_last_resort_no">Inget reservpaket. När engångspaketen tar slut misslyckas inbjudningar.</string> - <string name="cordn_keypackages_last_resort_yes">Ett reservpaket är publicerat, så en inbjudan kan fortfarande göras när engångspaketen tagit slut.</string> - <plurals name="cordn_keypackages_orphans"> - <item quantity="one">%1$d paket som den här enheten inte kan öppna</item> - <item quantity="other">%1$d paket som den här enheten inte kan öppna</item> - </plurals> - <string name="cordn_keypackages_orphans_body">Koordinatorn lämnar ut dessa till den som bjuder in dig, men den privata halvan finns inte på den här enheten — den tillhör en annan installation. Om den installationen är borta ger en inbjudan med något av dessa ett Welcome-meddelande som ingen någonsin kan öppna.</string> - <string name="cordn_keypackages_publish">Publicera ett</string> - <string name="cordn_keypackages_publish_last_resort">Publicera reservpaket</string> - <plurals name="cordn_keypackages_summary"> - <item quantity="one">%1$d engångspaket tillgängligt.</item> - <item quantity="other">%1$d engångspaket tillgängliga.</item> - </plurals> - <string name="cordn_keypackages_topup_note">När du väl har publicerat här ersätter Amethyst tyst paketen allteftersom de används. Det första publicerar den aldrig åt dig.</string> - <string name="cordn_keypackages_withdraw_all">Återkalla alla</string> - <string name="cordn_keypackages_withdraw_orphans">Återkalla dem</string> - <string name="cordn_link_request">Be att få gå med i gruppen</string> - <string name="cordn_link_request_disclosure">Detta publicerar ett nyckelpaket med din egen kontonyckel och talar om för koordinatorn att du vill gå med i gruppen — oavsett om någon svarar.</string> - <string name="cordn_link_request_failed">Kunde inte be att få gå med.</string> - <string name="cordn_link_request_sent">Förfrågan skickad. En medlem i gruppen måste lägga till dig; inbjudan dyker upp under cordn-inbjudningar.</string> - <string name="cordn_link_scan">Skanna</string> - <string name="cordn_media_download_failed">Kunde inte öppna den filen.</string> - <string name="cordn_media_no_server">Ingen mediaserver är inställd för det här kontot, så filen har ingenstans att ta vägen. Välj en i Inställningar.</string> - <string name="cordn_media_unreadable">Filen kunde inte läsas.</string> - <string name="cordn_media_upload_failed">Kunde inte skicka den filen.</string> - <plurals name="cordn_member_count"> - <item quantity="one">%1$d medlem</item> - <item quantity="other">%1$d medlemmar</item> - </plurals> - <string name="cordn_message_deleted">Meddelandet raderat</string> - <string name="cordn_message_details_coordinator">Koordinator</string> - <string name="cordn_message_details_cursor">Markör</string> - <string name="cordn_message_details_sent_at">Skickat</string> - <string name="cordn_message_details_title">Meddelandedetaljer</string> - <string name="cordn_message_edited">redigerat</string> - <string name="cordn_pinned_by">Fäst av %1$s</string> - <plurals name="cordn_pinned_count"> - <item quantity="one">%1$d fäst meddelande i den här gruppen</item> - <item quantity="other">%1$d fästa meddelanden i den här gruppen</item> - </plurals> - <string name="cordn_pinned_next">Nästa fästa meddelande</string> - <string name="cordn_pinned_previous">Föregående fästa meddelande</string> - <string name="cordn_pinned_show_all">Visa alla fästa meddelanden</string> - <string name="cordn_pinned_title">Fästa meddelanden</string> - <string name="cordn_reactions_title">Reaktioner</string> - <string name="cordn_requests_accept">Lägg till</string> - <string name="cordn_requests_admin_only">Bara en admin kan godkänna. I en grupp utan admins är det alla.</string> - <string name="cordn_requests_check">Sök efter förfrågningar</string> - <string name="cordn_requests_decline">Avfärda</string> - <string name="cordn_requests_failed">Kunde inte läsa förfrågningarna.</string> - <string name="cordn_requests_none">Ingen väntar på att få gå med.</string> - <string name="cordn_requests_title">Förfrågningar om att gå med</string> - <string name="cordn_send">Skicka</string> - <string name="cordn_send_failed">Det gick inte att skicka.</string> - <string name="cordn_send_no_session">Den här gruppens koordinator är inte öppen, så inget kan skickas ännu.</string> - <string name="cordn_share_copied">Kopierad</string> - <string name="cordn_share_copy">Kopiera länk</string> - <string name="cordn_share_explainer">Alla som har den här länken kan be att få gå med. En admin måste fortfarande släppa in dem.</string> - <string name="cordn_share_title">Dela den här gruppen</string> - <string name="cordn_voice_play">Spela upp röstmeddelande</string> - <string name="cordn_voice_record">Spela in ett röstmeddelande</string> - <string name="cordn_voice_stop">Stoppa och skicka</string> - <string name="qr_scanner_camera_rationale">Amethyst behöver kameran för att skanna QR-koder. Inget spelas in eller laddas upp — bilderna avkodas på enheten och kastas sedan.</string> - <string name="qr_scanner_grant_camera">Tillåt kamera</string> - <string name="qr_scanner_camera_blocked">Kameraåtkomst är avstängd för Amethyst. Du kan slå på den igen i Androids inställningar.</string> - <string name="qr_scanner_open_settings">Öppna inställningar</string> - <string name="qr_scanner_unavailable">QR-avkodaren kunde inte starta på den här enheten. Du kan fortfarande klistra in koden som text.</string> - <string name="qr_scanner_torch_on">Tänd lampan</string> - <string name="qr_scanner_torch_off">Släck lampan</string> - <string name="qr_scanner_dark_hint">Det är mörkt — prova lampan</string> - <string name="qr_scanner_scan_image">Skanna en bild</string> - <string name="qr_scanner_paste">Klistra in</string> - <string name="qr_scanner_no_code_in_image">Ingen QR-kod hittades i bilden</string> - <string name="qr_scanner_clipboard_empty">Det finns inget i urklipp att skanna</string> - <string name="qr_scanner_pick_one">Tryck på koden du vill ha</string> - <plurals name="qr_scanner_sequence_progress"> - <item quantity="one">Fångade %1$s av %2$s del — fortsätt</item> - <item quantity="other">Fångade %1$s av %2$s delar — fortsätt</item> - </plurals> - <string name="qr_scanner_zoom_reset">Återställ zoom</string> - <string name="qr_scanner_unsupported_title">Amethyst kan inte öppna den här koden</string> - <string name="qr_scanner_unsupported_secret">Den här koden innehåller en privat nyckel eller en parningshemlighet, så innehållet visas inte här. Använd skärmen som förväntar sig den.</string> - <string name="qr_scanner_kind_nsec">Det här är en privat nyckel. Klistra in den på inloggningsskärmen i stället.</string> - <string name="qr_scanner_kind_wallet">Det här är en plånboksanslutning. Lägg till den under Plånboksinställningar.</string> - <string name="qr_scanner_kind_bunker">Det här är en fjärrsignatäradress. Amethyst delar ut sådana så att andra appar kan be den signera — den kan inte logga in med en.</string> - <string name="qr_scanner_kind_signer">Det här är en app som vill ansluta till din signatär. Öppna den från NIP-46-signatärskärmen.</string> - <string name="qr_scanner_kind_lightning">Det här är en Lightning-faktura.</string> - <string name="qr_scanner_kind_cashu">Det här är en Cashu-token.</string> - <string name="qr_scanner_kind_web">Det här är en webblänk.</string> - <string name="qr_scanner_kind_text">Det här är vanlig text, inte en Nostr-kod.</string> - <string name="qr_scanner_kind_nostr_unsupported">Det här är en Nostr-kod, men inte en som den här skärmen kan öppna.</string> - <string name="qr_scanner_label_profile">Profil</string> - <string name="qr_scanner_label_note">Anteckning</string> - <string name="qr_scanner_label_nostr">Nostr-kod</string> - <string name="qr_scanner_label_relay">Relä</string> - <string name="qr_scanner_label_nsec">Privat nyckel</string> - <string name="qr_scanner_label_wallet">Plånboksanslutning</string> - <string name="qr_scanner_label_bunker">Fjärrsignatäradress</string> - <string name="qr_scanner_label_signer">Anslutningsbegäran till signatär</string> - <string name="qr_scanner_label_lightning">Lightning-faktura</string> - <string name="qr_scanner_label_cashu">Cashu-token</string> - <string name="qr_scanner_label_web">Webblänk</string> - <string name="qr_scanner_label_text">Vanlig text</string> - <string name="qr_scanner_open_link">Öppna länk</string> - <string name="qr_scanner_copy">Kopiera</string> - <string name="qr_scanner_copied">Kopierat</string> - <string name="qr_scanner_try_again">Skanna igen</string> - <string name="workout_suggestion_connect_title">Använd träningspassen du redan har registrerat</string> - <string name="workout_suggestion_connect_message">Anslut Health Connect (Samsung Health, Google Fit, Fitbit, Garmin…) för att bygga Min träning, din egen träningssammanfattning, och för att fylla i det här passet från ett som din klocka redan har registrerat i stället för att skriva in varje siffra för hand.</string> - <string name="health_connect_rationale_headline">Amethyst använder Health Connect för att bygga Min träning — din egen träningssammanfattning, på din telefon.</string> - <string name="health_connect_rationale_intro">Min träning (meny → Du → Min träning) visar hur du har tränat: hur mycket du gjorde den här veckan jämfört med förra, hur din tid fördelas mellan löpning, cykling, promenader, simning och gym, dina bästa prestationer, hur många dagar du tränade och din nuvarande svit. Amethyst bygger den bilden från träningspassen som din klocka eller träningsapp redan har sparat i Health Connect, tillsammans med pass du har loggat i Amethyst. Allt räknas ut på din telefon och visas för dig.</string> - <string name="health_connect_rationale_exercise">Träning · vilken aktivitet du utförde, när den började och hur länge den varade. Det här är själva passet: det styr antalet pass, din träningstid, uppdelningen per aktivitet, dina aktiva dagar och din svit.</string> - <string name="health_connect_rationale_distance">Distans · hur långt du tog dig, visat som din distans den här veckan jämfört med förra, ditt veckosnitt, din distans per aktivitet och din längsta distans.</string> - <string name="health_connect_rationale_calories">Aktiva och totala kalorier · energin ett pass förbrände, visat som din energi den här veckan och hur den står sig mot förra veckan. Aktiva kalorier används när din källa registrerar dem; totala kalorier används som reserv för källor som bara registrerar total energi.</string> - <string name="health_connect_rationale_heart_rate">Puls · genomsnitt och maxvärde för varje pass, sammanslaget till din genomsnittliga och högsta puls för perioden. Det är så du skiljer en lätt vecka från en tuff med samma distans.</string> - <string name="health_connect_rationale_steps">Steg · antalet steg under en löptur, promenad eller vandring, visat som ditt veckosnitt för steg och ditt högsta antal steg.</string> - <string name="health_connect_rationale_elevation">Höjdmeter · hur mycket du klättrade, visat som ditt veckosnitt för stigning och din största stigning. Det är det som skiljer en platt vecka från en kuperad.</string> - <string name="health_connect_rationale_limit_window">Läser bara träningspass från de senaste 4 veckorna, och bara medan Min träning eller passredigeraren är öppen. Läser aldrig i bakgrunden.</string> - <string name="health_connect_rationale_limit_publish">Skickar aldrig dina hälsodata någonstans. Min träning räknas ut på din telefon och stannar där. Om du senare vill publicera ett pass trycker du på Dela för det passet, läser inlägget som Amethyst fyllt i och publicerar det själv — ett pass i taget, aldrig automatiskt.</string> - <string name="health_connect_rationale_limit_optional">Hela funktionen är valfri. Återkalla behörigheterna i Health Connect när som helst — Min träning går då tillbaka till sin uppmaning — eller stäng av förslagen i redigeraren under Inställningar → Skrivinställningar. Resten av Amethyst fortsätter att fungera.</string> - <string name="my_fitness_title">Min träning</string> - <string name="my_fitness_window">Senaste 4 veckorna</string> - <string name="my_fitness_loading_metrics">Läser in distans, kalorier och puls…</string> - <string name="my_fitness_window_note">De senaste fyra hela veckorna av din träning: passen du har publicerat, plus allt Health Connect registrerar på den här telefonen. Amethyst räknar ut sammanfattningen på enheten och publicerar ingenting av den.</string> - <string name="my_fitness_this_week">Den här veckan</string> - <string name="my_fitness_vs_last_week">jämfört med förra veckan</string> - <string name="my_fitness_weekly_average">Veckosnitt</string> - <string name="my_fitness_workouts">Pass</string> - <string name="my_fitness_active_days">Aktiva dagar</string> - <string name="my_fitness_streak">Dagar i rad</string> - <string name="my_fitness_time">Tid</string> - <string name="my_fitness_duration_hours_minutes">%1$d h %2$d min</string> - <string name="my_fitness_duration_minutes">%1$d min</string> - <string name="my_fitness_duration_seconds">%1$d s</string> - <string name="my_fitness_distance">Distans</string> - <string name="my_fitness_calories">Kalorier</string> - <string name="my_fitness_steps">Steg</string> - <string name="my_fitness_elevation">Stigning</string> - <string name="my_fitness_avg_heart_rate">Snittpuls</string> - <string name="my_fitness_max_heart_rate">Maxpuls</string> - <string name="my_fitness_by_activity">Per aktivitet</string> - <string name="my_fitness_bests">Bästa prestationer</string> - <string name="my_fitness_best_longest_distance">Längsta distans</string> - <string name="my_fitness_best_longest_duration">Längsta pass</string> - <string name="my_fitness_best_biggest_climb">Största stigning</string> - <string name="my_fitness_best_most_steps">Flest steg</string> - <string name="my_fitness_best_highest_heart_rate">Högsta puls</string> - <string name="my_fitness_recent">Senaste passen</string> - <string name="my_fitness_empty">Inget loggat de senaste 4 veckorna. Pass du publicerar, och de som din klocka registrerar, visas här.</string> - <string name="my_fitness_connect_title">Följ din träning</string> - <string name="my_fitness_connect_message">Anslut Health Connect för att se dina veckosummor, utvecklingen vecka för vecka, bästa prestationer och träningssvit. Allt stannar på din telefon.</string> - <string name="my_fitness_connect_banner">Anslut Health Connect för att lägga till passen din klocka registrerar, med puls, steg och stigning.</string> - <string name="my_fitness_connect_button">Anslut</string> - <string name="my_fitness_share">Dela det här passet</string> - <string name="my_fitness_unit_bpm">bpm</string> - <string name="my_fitness_unit_kcal">kcal</string> - <string name="my_fitness_unit_km">km</string> - <string name="my_fitness_unit_mi">mi</string> - <string name="my_fitness_unit_m">m</string> - <string name="my_fitness_unit_ft">ft</string> - <string name="looking_for_event_title">Läser in</string> - <string name="looking_for_private_event">Väntar på att dina privata meddelanden ska läsas in…</string> - <string name="call_duration_minutes_seconds">%1$02d:%2$02d</string> - <string name="call_duration_hours_minutes_seconds">%1$d:%2$02d:%3$02d</string> - <string name="marmot_preview_group_updated">Gruppen uppdaterad</string> - <string name="marmot_preview_media">Bilaga</string> - <string name="marmot_preview_no_text">Meddelande</string> - <string name="marmot_preview_with_sender">%1$s: %2$s</string> - <string name="marmot_empty_create_action">Skapa en grupp</string> - <string name="marmot_invite_device_banner">Marmot-inbjudningar går till en annan enhet och kan inte öppnas här.</string> - <string name="marmot_invite_device_banner_dismiss">Avfärda</string> - <string name="marmot_invite_device_publish">Publicera från den här enheten</string> - <string name="wallet_open_zapped_note">Öppna zappad anteckning</string> - <string name="video_player_settings_action_captions">Undertexter</string> - <string name="video_player_settings_action_captions_description">Slå på eller av undertexter (dolt när videon saknar sådana)</string> - <string name="app_notification_reply_failed">Inte skickat</string> - <string name="app_notification_reply_retry">Försök igen</string> - <string name="app_notification_reply_sending">Skickar…</string> - <string name="app_notification_reply_unconfirmed">Skickar fortfarande…</string> - <string name="app_notification_shortcut_account_removed">Det här kontot har tagits bort från Amethyst.</string> - <string name="captions_off">Av</string> - <string name="captions_turn_off">Stäng av undertexter</string> - <string name="captions_turn_on">Slå på undertexter</string> - <string name="captions_unnamed_track">Undertexter</string> - <string name="kind_app_curation_set">Applista</string> - <string name="kind_article_curation_set">Artikellista</string> - <string name="kind_bookmark_set">Bokmärkessamling</string> - <string name="kind_favorite_algo_feeds">Favoritflöden</string> - <string name="kind_git_authors">Git-författare</string> - <string name="kind_git_repositories">Git-förråd</string> - <string name="kind_good_wiki_authors">Betrodda wikiförfattare</string> - <string name="kind_good_wiki_relays">Betrodda wikireläer</string> - <string name="kind_interest_set">Intressen</string> - <string name="kind_media_follows">Mediaföljningar</string> - <string name="kind_media_starter_pack">Startpaket för media</string> - <string name="kind_mute_set_for_kind">Tystat för typ %1$s</string> - <string name="kind_picture_curation_set">Bildlista</string> - <string name="kind_simple_groups">Grupper</string> - <string name="kind_video_collaboration">Videosamarbete</string> - <string name="kind_video_list">Videolista</string> - <string name="kind_video_subtitles">Videoundertexter</string> - <string name="login_bunker_not_supported">Amethyst för Android kan inte logga in med en bunker://-adress. Den delar ut sådana så att andra appar kan be den signera, men den kan inte logga in med en. Använd en extern signatärapp eller Amethyst Desktop.</string> - <string name="login_nostrconnect_not_supported">Det där är en app som vill ansluta till din signatär, inte ett sätt att logga in. Logga in först och öppna den sedan från NIP-46-signatärskärmen.</string> - <string name="my_fitness">Min träning</string> - <string name="nip51_list_has_private_members">Den här listan har privata medlemmar som bara dess skapare kan se</string> - <string name="route_edit_geocache">Redigera cache</string> - <string name="route_edit_geocache_hunt">Redigera jakt</string> - <string name="route_geocache_detail">Cache</string> - <string name="route_geocache_hunt_detail">Jakt</string> - <string name="route_geocache_hunts">Cachejakter</string> - <string name="route_geocaches">Geocacher</string> - <string name="route_log_geocache_find">Logga ett fynd</string> - <string name="route_new_geocache">Göm en cache</string> - <string name="route_new_geocache_hunt">Ny jakt</string> - <string name="video_collaboration_accepted_on">Accepterade ett samarbetsomnämnande</string> - <string name="video_collaboration_declined_on">Avböjde ett samarbetsomnämnande</string> - <string name="video_credit_accepted">Omnämnande accepterat</string> - <string name="video_credit_audio_from">Ljud från</string> - <string name="video_credit_featuring">Med</string> - <string name="video_credit_inspired_by">Inspirerad av</string> - <string name="video_credit_references">Referenser</string> - <string name="video_curation_set_private_items">Privat lista</string> - <plurals name="video_curation_set_video_count"> - <item quantity="one">%1$d video</item> - <item quantity="other">%1$d videor</item> - </plurals> - <string name="marmot_invite_device_success">Den här enheten tar nu emot dina Marmot-inbjudningar.</string> - <string name="marmot_invite_device_failure">Det gick inte att publicera KeyPackage: %1$s</string> - <string name="marmot_invite_device_rejected">Inget relä accepterade KeyPackage. Kontrollera dina reläinställningar och försök igen.</string> - <string name="chat_delivery_sending">Skickar…</string> - <string name="chat_delivery_failed">Kunde inte skickas. Tryck för att försöka igen.</string> - <plurals name="marmot_unread_messages"> - <item quantity="one">%1$d nytt meddelande</item> - <item quantity="other">%1$d nya meddelanden</item> - </plurals> - <string name="chat_type_cordn_title">Cordn-grupper</string> - <string name="chat_type_cordn_desc">End-to-end-krypterade grupper som ordnas av en koordinator du väljer, i stället för av reläer.</string> - <string name="new_conversation_cordn_title">Cordn-grupp</string> - <string name="new_conversation_cordn_tagline">Krypterad gruppchatt som ordnas av en koordinator du väljer.</string> - <string name="new_conversation_cordn_chip">Koordinerad</string> - <string name="new_conversation_cordn_best">Bäst för team som vill ha en pålitlig ordning i konversationen.</string> - <string name="new_conversation_cordn_cta">Skapa Cordn-grupp</string> - <string name="new_conversation_cordn_pro_1">End-to-end-krypterad med MLS — koordinatorn kan aldrig läsa ett meddelande.</string> - <string name="new_conversation_cordn_pro_2">En gemensam ordning för alla, så att historiken inte kan kastas om av en felaktig klocka.</string> - <string name="new_conversation_cordn_con_1">Koordinatorn får veta vilka som är med i gruppen och när ni pratar, även om den inte kan läsa vad ni säger.</string> <string name="sno_object_title">3D-objekt</string> <string name="sno_object_details">%1$d hörn · %2$d ytor</string> <string name="sno_object_unreadable">Det här 3D-objektet kunde inte läsas (regel %1$s)</string> @@ -5662,6 +5397,7 @@ <item quantity="one">%1$d byte av något som den här klienten inte läser</item> <item quantity="other">%1$d byte av något som den här klienten inte läser</item> </plurals> + <!-- Browser pill (redesign prototypes, see amethyst/plans/2026-09-26-browser-ui-review.md) --> <string name="browser_pill_close">Stäng</string> <string name="browser_pill_tile_copy">Kopiera länk</string> <string name="browser_pill_tile_find">Sök</string> @@ -6092,4 +5828,268 @@ <string name="backup_type_trust_providers_explainer">Tjänsterna du litar på för att rangordna och verifiera personer åt dig.</string> <string name="backup_type_trusted_relays">lista över betrodda reläer</string> <string name="backup_type_trusted_relays_explainer">Reläer du litar på att ansluta till utan att fråga.</string> + <string name="cordn_action_delete">Radera</string> + <string name="cordn_action_edit">Redigera</string> + <string name="cordn_action_editing">Redigerar ditt meddelande</string> + <string name="cordn_action_go_to_message">Gå till meddelandet</string> + <string name="cordn_action_pin">Fäst</string> + <string name="cordn_action_reply">Svara</string> + <string name="cordn_action_unpin">Lossa</string> + <string name="cordn_admin_action_failed">Ändringen kunde inte genomföras.</string> + <string name="cordn_admin_coordinator_silent">Koordinatorn svarade inte, så inget ändrades. Försök igen om en stund.</string> + <string name="cordn_admin_no_key_package">%1$s har inte publicerat något nyckelpaket till den här koordinatorn, så de kan inte läggas till ännu. Be dem att först öppna cordn på den här koordinatorn.</string> + <string name="cordn_admin_no_self_remove">Du kan inte ta bort dig själv från en cordn-grupp.</string> + <string name="cordn_admin_no_welcome">%1$s kunde inte ges någon väg in i gruppen, så tillägget avbröts.</string> + <string name="cordn_admin_not_a_member">%1$s är inte med i den här gruppen.</string> + <string name="cordn_admin_someone">Den personen</string> + <string name="cordn_admin_wrong_key_package">Den här koordinatorn lämnade ut ett nyckelpaket som tillhör någon annan, så %1$s lades inte till. Inget ändrades.</string> + <string name="cordn_backup_contents_body">Dina gruppkrypteringsnycklar, dina nyckelpaket och varje meddelande i de grupperna. Den som öppnar filen kan läsa konversationerna. Lösenfrasen är det enda som skyddar den när den väl lämnat Amethyst.</string> + <string name="cordn_backup_contents_title">Vad filen innehåller</string> + <string name="cordn_backup_explainer">Din nsec kan inte återskapa cordn-grupper. En cordn-grupp lever som krypteringstillstånd på den här enheten och som en ordnad ström hos en koordinator som inte kan läsa den — förlorar du enheten utan säkerhetskopia är gruppen borta, inklusive allt som redan sagts i den.</string> + <string name="cordn_backup_export">Exportera</string> + <string name="cordn_backup_exported">Säkerhetskopian sparad.</string> + <string name="cordn_backup_failed">Det fungerade inte. Kontrollera lösenfrasen och filen.</string> + <string name="cordn_backup_passphrase">Lösenfras</string> + <string name="cordn_backup_restore">Återställ</string> + <string name="cordn_backup_restore_body">Återställning ersätter cordn-grupperna på den här enheten med dem i filen. Den är till för en telefon som tagit över från en annan — inte ett sätt att använda samma grupper på två enheter samtidigt.</string> + <string name="cordn_backup_restore_confirm_body">Alla cordn-grupper som nu finns på den här enheten tas bort och ersätts med dem i filen.\n\nÅterställ inte till en andra telefon medan den första fortfarande används. Båda skulle ha samma gruppnycklar, och så snart båda har skickat ett meddelande går gruppen sönder för alla — tyst, och utan väg tillbaka.</string> + <string name="cordn_backup_restore_confirm_title">Ersätta den här enhetens cordn-grupper?</string> + <string name="cordn_backup_restore_title">Återställ</string> + <string name="cordn_backup_restored">Återställd.</string> + <string name="cordn_chat_empty_description">Allt som skickas här krypteras för den här gruppen. Koordinatorn förmedlar det utan att kunna läsa det.</string> + <string name="cordn_chat_empty_title">Inga meddelanden ännu</string> + <string name="cordn_chat_unread_divider">Nya meddelanden</string> + <string name="cordn_chat_yesterday">I går</string> + <string name="cordn_composer_hint">Meddelande</string> + <string name="cordn_coordinators_add_action">Lägg till</string> + <string name="cordn_coordinators_add_failed">Kunde inte öppna den koordinatorn.</string> + <string name="cordn_coordinators_copy_key">Kopiera koordinatorns npub</string> + <string name="cordn_coordinators_discover_action">Leta efter koordinatorer</string> + <string name="cordn_coordinators_discover_add">Lägg till</string> + <string name="cordn_coordinators_discover_explainer">Läser annonser som dina reläer redan har. Ingen koordinator kontaktas, så ingen av dem får veta att du letade.</string> + <string name="cordn_coordinators_discover_failed">Kunde inte läsa annonser</string> + <string name="cordn_coordinators_discover_none">Ingen annonserar på dina reläer.</string> + <string name="cordn_coordinators_discover_seen">Annonserade för %1$s sedan</string> + <string name="cordn_coordinators_discover_seen_on">Senast annonserad %1$s</string> + <plurals name="cordn_coordinators_discover_unheard"> + <item quantity="one">Inget hittades, och %1$d av dina reläer svarade inte.</item> + <item quantity="other">Inget hittades, och %1$d av dina reläer svarade inte.</item> + </plurals> + <string name="cordn_coordinators_explainer">En koordinator ordnar en grupps meddelanden och är det enda som kan göra det. Förlorar man den förloras ordningen, och en andra koordinator speglar inte den första — så det här är namngivna saker du väljer, inte utbytbara reläer.</string> + <plurals name="cordn_coordinators_health_down"> + <item quantity="one">Svarar inte — %1$d anrop i rad har misslyckats. Grupper på den synkroniseras inte.</item> + <item quantity="other">Svarar inte — %1$d anrop i rad har misslyckats. Grupper på den synkroniseras inte.</item> + </plurals> + <string name="cordn_coordinators_health_ok">Svarar.</string> + <string name="cordn_coordinators_health_retrying">Ett anrop misslyckades; försöker igen.</string> + <string name="cordn_coordinators_health_unknown">Inget har efterfrågats ännu.</string> + <string name="cordn_coordinators_hide_older">Dölj dem som slutat annonsera</string> + <string name="cordn_coordinators_identify">Fråga vem den är</string> + <string name="cordn_coordinators_key">Koordinatornyckel</string> + <string name="cordn_coordinators_label">Ett namn för den (ditt eget, valfritt)</string> + <string name="cordn_coordinators_label_note">Det här namnet är ditt och stannar på den här enheten. En koordinator kan inte bevisa ett namn, så den ombeds aldrig uppge något.</string> + <string name="cordn_coordinators_more">Fler åtgärder</string> + <string name="cordn_coordinators_none">Inga koordinatorer ännu.</string> + <string name="cordn_coordinators_purge">Rensa bort</string> + <string name="cordn_coordinators_purge_body">Att ta bort behåller grupperna på den här enheten, så om du lägger till koordinatorn igen kommer de tillbaka. Att rensa bort raderar dem: gruppnycklarna, lässtatusen och dina nyckelpaket försvinner, och de grupperna kan bara nås igen via en ny inbjudan.\n\nKoordinatorn meddelas inte och behåller allt den redan hade.</string> + <string name="cordn_coordinators_purge_title">Rensa bort den här koordinatorn?</string> + <string name="cordn_coordinators_relays">%1$s</string> + <string name="cordn_coordinators_relays_label">Svarar på</string> + <string name="cordn_coordinators_relays_more">%1$s +%2$d till</string> + <string name="cordn_coordinators_remove">Ta bort</string> + <string name="cordn_coordinators_rename">Byt namn</string> + <string name="cordn_coordinators_server">Säger sig vara: %1$s</string> + <string name="cordn_coordinators_server_claim">En koordinators namn och version är strängar den själv valt. Dess publika nyckel är det enda som identifierar den.</string> + <string name="cordn_coordinators_server_silent">Svarade, men angav inget namn.</string> + <string name="cordn_coordinators_show_all">Visa alla %1$d</string> + <string name="cordn_coordinators_show_fewer">Visa färre</string> + <plurals name="cordn_coordinators_show_older"> + <item quantity="one">Visa %1$d som slutat annonsera</item> + <item quantity="other">Visa %1$d som slutat annonsera</item> + </plurals> + <string name="cordn_coordinators_stale_note">Dessa har inte annonserat på över en månad. Att skapa en grupp på en som försvunnit kommer att misslyckas.</string> + <string name="cordn_create_action">Skapa grupp</string> + <plurals name="cordn_create_action_invite"> + <item quantity="one">Skapa och bjud in %1$d person</item> + <item quantity="other">Skapa och bjud in %1$d personer</item> + </plurals> + <string name="cordn_create_add_member">Lägg till någon</string> + <string name="cordn_create_admin">Admin</string> + <plurals name="cordn_create_admin_count"> + <item quantity="one">%1$d person kan lägga till och ta bort</item> + <item quantity="other">%1$d personer kan lägga till och ta bort</item> + </plurals> + <string name="cordn_create_admin_only_me">Bara jag hanterar den här gruppen</string> + <string name="cordn_create_admin_only_me_on">Bara du kan bjuda in, ta bort medlemmar eller ändra gruppens uppgifter. Du kan lämna över det till någon annan senare.</string> + <string name="cordn_create_coordinator">Koordinator</string> + <string name="cordn_create_coordinator_change">Ändra</string> + <string name="cordn_create_coordinator_discover">Hitta koordinatorer</string> + <string name="cordn_create_coordinator_hide">Klar</string> + <string name="cordn_create_coordinator_new">En annan koordinator</string> + <string name="cordn_create_coordinator_pubkey">Koordinatorns publika nyckel (hex eller npub)</string> + <string name="cordn_create_coordinator_relays">Reläer den svarar på, ett per rad</string> + <plurals name="cordn_create_coverage_all"> + <item quantity="one">Når %1$d person — alla du valt</item> + <item quantity="other">Når alla dina %1$d personer</item> + </plurals> + <string name="cordn_create_coverage_partial">Når %1$d av %2$d — resten skulle bli utanför</string> + <string name="cordn_create_coverage_unknown">Inte kontrollerad — den tillfrågas om du väljer den</string> + <string name="cordn_create_description">Beskrivning (valfritt)</string> + <string name="cordn_create_egalitarian">Alla kan lägga till och ta bort</string> + <string name="cordn_create_egalitarian_note">Alla i gruppen kan bjuda in, ta bort medlemmar och ändra gruppens uppgifter. Vilken medlem som helst kan utse admins senare, vilket upphäver detta.</string> + <string name="cordn_create_explainer">En cordn-grupp ordnas av en koordinator du väljer. Gruppen skapas på den här enheten — koordinatorn får veta att gruppen finns först när du bjuder in någon eller skickar ett meddelande.</string> + <string name="cordn_create_failed">Gruppen kunde inte skapas.</string> + <string name="cordn_create_name">Gruppnamn</string> + <string name="cordn_create_no_coordinator">Ingen koordinator vald</string> + <string name="cordn_create_outcome_explainer">Varje inbjudan är ett Welcome-meddelande som lämnas hos koordinatorn, så de kan misslyckas en i taget. Ingen har meddelats: de ser det nästa gång de öppnar sin app.</string> + <string name="cordn_create_outcome_failed">Koordinatorn avvisade inbjudan</string> + <string name="cordn_create_outcome_link">Skicka en länk</string> + <string name="cordn_create_outcome_no_welcome">Tillagd i gruppen, men inbjudan nådde inte fram — försök igen från gruppskärmen</string> + <string name="cordn_create_outcome_open">Öppna chatten</string> + <string name="cordn_create_outcome_skipped">Ingen nyckel här, så inget Welcome-meddelande kunde lämnas</string> + <string name="cordn_create_outcome_title">Gruppen är skapad</string> + <string name="cordn_create_outcome_waiting">Väntar på dem</string> + <string name="cordn_create_people_done">Klar</string> + <string name="cordn_create_people_empty">Ingen tillagd ännu. En grupp kan skapas tom och inbjudningar skickas senare, men att ange personer nu är det som visar vilken koordinator som kan nå dem.</string> + <string name="cordn_create_people_none">Ingen ännu — tryck för att lägga till personer</string> + <string name="cordn_create_people_note">Att ange personer nu är det som talar om för oss vilken koordinator som kan nå dem. Var och en får ett Welcome-meddelande som lämnas hos den koordinatorn, och som de hämtar nästa gång de öppnar sin app.</string> + <plurals name="cordn_create_reach_count"> + <item quantity="one">Nåbar via %1$d koordinator</item> + <item quantity="other">Nåbar via %1$d koordinatorer</item> + </plurals> + <string name="cordn_create_reach_none">Ingen nyckel hos någon koordinator du använder</string> + <string name="cordn_create_reach_unknown">Inte kontrollerad ännu</string> + <string name="cordn_create_step_admins">Vem som kan lägga till och ta bort</string> + <string name="cordn_create_step_name">Namnge den</string> + <string name="cordn_create_step_people">Vilka som är med</string> + <string name="cordn_create_step_where">Var den finns</string> + <string name="cordn_create_title">Ny Cordn-grupp</string> + <plurals name="cordn_create_unreachable_count"> + <item quantity="one">%1$d person kan inte nås av någon koordinator du använder</item> + <item quantity="other">%1$d personer kan inte nås av någon koordinator du använder</item> + </plurals> + <string name="cordn_create_you">Du, alltid admin</string> + <string name="cordn_delete_confirm_body">Alla i gruppen kommer att se meddelandet som återkallat, och Amethyst slutar visa texten. Koordinatorn har fortfarande den krypterade kopia som skickades, och den som redan har läst det har redan läst det.</string> + <string name="cordn_delete_confirm_title">Återkalla meddelande</string> + <string name="cordn_delivery_accepted">Accepterat av koordinatorn</string> + <string name="cordn_exposure_close">Uppfattat</string> + <string name="cordn_exposure_open">Vad den här koordinatorn kan se</string> + <string name="cordn_group_info">Gruppinfo</string> + <string name="cordn_group_unavailable">Den här gruppen är inte tillgänglig. Dess koordinator kan ha tagits bort.</string> + <string name="cordn_group_unavailable_detail">Lägg till en koordinator i Inställningar för att börja använda cordn-grupper.</string> + <plurals name="cordn_groups_count"> + <item quantity="one">%1$d grupp</item> + <item quantity="other">%1$d grupper</item> + </plurals> + <string name="cordn_groups_down_note">Inget kommer fram från de här grupperna. Amethyst fortsätter att försöka.</string> + <string name="cordn_groups_manage">Koordinatorer</string> + <string name="cordn_groups_none">Inga cordn-grupper ännu</string> + <string name="cordn_groups_none_detail">En cordn-grupp finns hos en koordinator som håller reda på medlemskapet och skickar meddelanden vidare, utan att någonsin se vad någon skriver.</string> + <string name="cordn_groups_start">Starta en</string> + <string name="cordn_info_add_member">Lägg till någon</string> + <string name="cordn_info_add_member_none">Ingen hittades med det namnet.</string> + <string name="cordn_info_add_member_note">De kan bara läggas till om de har publicerat ett nyckelpaket till den här koordinatorn.</string> + <string name="cordn_info_add_member_placeholder">Namn, npub eller namn@domän</string> + <string name="cordn_info_admin_badge">Admin</string> + <plurals name="cordn_info_admins_kept"> + <item quantity="one">Gruppens %1$d admin förblir oförändrad.</item> + <item quantity="other">Gruppens %1$d admins förblir oförändrade.</item> + </plurals> + <string name="cordn_info_coordinator">Koordinator</string> + <string name="cordn_info_coordinator_key">Koordinatornyckel</string> + <string name="cordn_info_coordinator_nprofile">Koordinatorlänk</string> + <string name="cordn_info_copy_nprofile">Kopiera koordinatorns nprofile</string> + <string name="cordn_info_edit_details">Redigera uppgifter</string> + <string name="cordn_info_egalitarian">Den här gruppen har inga admins: alla kan lägga till, ta bort och byta namn, permanent.</string> + <string name="cordn_info_epoch">Epok</string> + <string name="cordn_info_gid">Grupp-id</string> + <string name="cordn_info_has_key_package">Kan läggas till via den här koordinatorn</string> + <string name="cordn_info_members">Medlemmar</string> + <string name="cordn_info_remove_confirm_body">%1$s slutar ta emot meddelanden härifrån. De behåller det de redan har läst, och detta kan inte ångras — de skulle behöva bjudas in igen.</string> + <string name="cordn_info_remove_confirm_title">Ta bort från gruppen?</string> + <string name="cordn_info_remove_member">Ta bort</string> + <string name="cordn_info_save">Spara</string> + <string name="cordn_info_technical">Tekniska detaljer</string> + <string name="cordn_invitations_accept">Gå med</string> + <string name="cordn_invitations_decline">Avböj</string> + <string name="cordn_invitations_decline_warning">Att avböja är permanent. För att komma in igen måste du bjudas in på nytt.</string> + <string name="cordn_invitations_explainer">Kontrolleras när du öppnar den här skärmen, och aldrig annars — att fråga en koordinator om någon har bjudit in dig avslöjar att du finns här, så Amethyst gör det inte i bakgrunden.</string> + <string name="cordn_invitations_load_failed">Kunde inte läsa inbjudningar.</string> + <plurals name="cordn_invitations_members"> + <item quantity="one">%1$d person är redan med i den här gruppen</item> + <item quantity="other">%1$d personer är redan med i den här gruppen</item> + </plurals> + <string name="cordn_invitations_members_more">+%1$d till</string> + <string name="cordn_invitations_no_coordinators">Du har ingen koordinator öppen. cordn-inbjudningar kommer via en koordinator, så det finns ingenstans att leta ännu.</string> + <string name="cordn_invitations_none">Inga väntande inbjudningar.</string> + <string name="cordn_invitations_refresh">Kontrollera igen</string> + <string name="cordn_invitations_skipped">En inbjudan lämnades till en annan enhet</string> + <string name="cordn_invitations_title">Cordn-inbjudningar</string> + <string name="cordn_invitations_unreachable">%1$s svarade inte</string> + <string name="cordn_invitations_via">Via %1$s</string> + <string name="cordn_keypackages_disclosure">Publicering signerar ett belägg med din egen kontonyckel. Den koordinatorn vet då att kontot finns och går att bjuda in — permanent, och oavsett om någon bjuder in dig.</string> + <string name="cordn_keypackages_explainer">Andra kan bara lägga till dig i en cordn-grupp genom att ta ett nyckelpaket du publicerat till den koordinatorn. cordn har ingen annan plats att förvara ett, så utan något publicerat kan ingen bjuda in dig — och inget talar om för dem varför.</string> + <string name="cordn_keypackages_failed">Kunde inte läsa nyckelpaketen.</string> + <string name="cordn_keypackages_last_resort_no">Inget reservpaket. När engångspaketen tar slut misslyckas inbjudningar.</string> + <string name="cordn_keypackages_last_resort_yes">Ett reservpaket är publicerat, så en inbjudan kan fortfarande göras när engångspaketen tagit slut.</string> + <plurals name="cordn_keypackages_orphans"> + <item quantity="one">%1$d paket som den här enheten inte kan öppna</item> + <item quantity="other">%1$d paket som den här enheten inte kan öppna</item> + </plurals> + <string name="cordn_keypackages_orphans_body">Koordinatorn lämnar ut dessa till den som bjuder in dig, men den privata halvan finns inte på den här enheten — den tillhör en annan installation. Om den installationen är borta ger en inbjudan med något av dessa ett Welcome-meddelande som ingen någonsin kan öppna.</string> + <string name="cordn_keypackages_publish">Publicera ett</string> + <string name="cordn_keypackages_publish_last_resort">Publicera reservpaket</string> + <plurals name="cordn_keypackages_summary"> + <item quantity="one">%1$d engångspaket tillgängligt.</item> + <item quantity="other">%1$d engångspaket tillgängliga.</item> + </plurals> + <string name="cordn_keypackages_topup_note">När du väl har publicerat här ersätter Amethyst tyst paketen allteftersom de används. Det första publicerar den aldrig åt dig.</string> + <string name="cordn_keypackages_withdraw_all">Återkalla alla</string> + <string name="cordn_keypackages_withdraw_orphans">Återkalla dem</string> + <string name="cordn_link_request">Be att få gå med i gruppen</string> + <string name="cordn_link_request_disclosure">Detta publicerar ett nyckelpaket med din egen kontonyckel och talar om för koordinatorn att du vill gå med i gruppen — oavsett om någon svarar.</string> + <string name="cordn_link_request_failed">Kunde inte be att få gå med.</string> + <string name="cordn_link_request_sent">Förfrågan skickad. En medlem i gruppen måste lägga till dig; inbjudan dyker upp under cordn-inbjudningar.</string> + <string name="cordn_link_scan">Skanna</string> + <string name="cordn_media_download_failed">Kunde inte öppna den filen.</string> + <string name="cordn_media_no_server">Ingen mediaserver är inställd för det här kontot, så filen har ingenstans att ta vägen. Välj en i Inställningar.</string> + <string name="cordn_media_unreadable">Filen kunde inte läsas.</string> + <string name="cordn_media_upload_failed">Kunde inte skicka den filen.</string> + <plurals name="cordn_member_count"> + <item quantity="one">%1$d medlem</item> + <item quantity="other">%1$d medlemmar</item> + </plurals> + <string name="cordn_message_deleted">Meddelandet raderat</string> + <string name="cordn_message_details_coordinator">Koordinator</string> + <string name="cordn_message_details_cursor">Markör</string> + <string name="cordn_message_details_sent_at">Skickat</string> + <string name="cordn_message_details_title">Meddelandedetaljer</string> + <string name="cordn_message_edited">redigerat</string> + <string name="cordn_pinned_by">Fäst av %1$s</string> + <plurals name="cordn_pinned_count"> + <item quantity="one">%1$d fäst meddelande i den här gruppen</item> + <item quantity="other">%1$d fästa meddelanden i den här gruppen</item> + </plurals> + <string name="cordn_pinned_next">Nästa fästa meddelande</string> + <string name="cordn_pinned_previous">Föregående fästa meddelande</string> + <string name="cordn_pinned_show_all">Visa alla fästa meddelanden</string> + <string name="cordn_pinned_title">Fästa meddelanden</string> + <string name="cordn_reactions_title">Reaktioner</string> + <string name="cordn_requests_accept">Lägg till</string> + <string name="cordn_requests_admin_only">Bara en admin kan godkänna. I en grupp utan admins är det alla.</string> + <string name="cordn_requests_check">Sök efter förfrågningar</string> + <string name="cordn_requests_decline">Avfärda</string> + <string name="cordn_requests_failed">Kunde inte läsa förfrågningarna.</string> + <string name="cordn_requests_none">Ingen väntar på att få gå med.</string> + <string name="cordn_requests_title">Förfrågningar om att gå med</string> + <string name="cordn_send">Skicka</string> + <string name="cordn_send_failed">Det gick inte att skicka.</string> + <string name="cordn_send_no_session">Den här gruppens koordinator är inte öppen, så inget kan skickas ännu.</string> + <string name="cordn_share_copied">Kopierad</string> + <string name="cordn_share_copy">Kopiera länk</string> + <string name="cordn_share_explainer">Alla som har den här länken kan be att få gå med. En admin måste fortfarande släppa in dem.</string> + <string name="cordn_share_title">Dela den här gruppen</string> + <string name="cordn_voice_play">Spela upp röstmeddelande</string> + <string name="cordn_voice_record">Spela in ett röstmeddelande</string> + <string name="cordn_voice_stop">Stoppa och skicka</string> </resources> diff --git a/docs/changelog/translators.json b/docs/changelog/translators.json index 18d1805a8c..0af8e0c262 100644 --- a/docs/changelog/translators.json +++ b/docs/changelog/translators.json @@ -123,6 +123,12 @@ "Vietnamese" ] }, + { + "user": "summoner001", + "languages": [ + "Hungarian" + ] + }, { "user": "maxblake2015", "languages": [ @@ -135,12 +141,6 @@ "Hindi" ] }, - { - "user": "summoner001", - "languages": [ - "Hungarian" - ] - }, { "user": "markkks", "languages": [ From 00102f5676fec11fdbc18506d8b06309496f074c Mon Sep 17 00:00:00 2001 From: davotoula <1747287+davotoula@users.noreply.github.com> Date: Sun, 27 Sep 2026 12:10:56 +0000 Subject: [PATCH 49/60] chore: sync Crowdin translations and seed translator npub placeholders --- docs/changelog/translators.json | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/changelog/translators.json b/docs/changelog/translators.json index 0af8e0c262..eb7c79c88a 100644 --- a/docs/changelog/translators.json +++ b/docs/changelog/translators.json @@ -147,6 +147,15 @@ "Russian" ] }, + { + "user": "davotoula", + "languages": [ + "Czech", + "German", + "Portuguese, Brazilian", + "Swedish" + ] + }, { "user": "greenart7c3", "languages": [] @@ -219,10 +228,6 @@ "user": "adhrasreoshiathoi", "languages": [] }, - { - "user": "davotoula", - "languages": [] - }, { "user": "crackadoo", "languages": [] From 5d67955aee14391076c0520f4bd3c76ff9797421 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sun, 27 Sep 2026 13:54:46 +0000 Subject: [PATCH 50/60] NIP-FE: remove /neg Upstream NIP-FE dropped negentropy over HTTP; NIP-77 stays on the websocket. HttpRelayCommand is REQ, COUNT and EVENT. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jDNhr6a3J4VC5TaG3Yd48 --- .../relay/server/backend/LiveEventStore.kt | 8 +-- .../nipFERelayOverHttp/HttpRelayCommand.kt | 16 ----- .../nipFERelayOverHttp/HttpRelayHandler.kt | 2 +- .../nipFERelayOverHttp/HttpRelayStatus.kt | 2 - .../HttpRelayHandlerTest.kt | 66 ++----------------- 5 files changed, 11 insertions(+), 83 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/LiveEventStore.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/LiveEventStore.kt index 1569cc194f..88da1daaed 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/LiveEventStore.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/LiveEventStore.kt @@ -409,10 +409,10 @@ class LiveEventStore( /** * Serves repeated NEG-OPENs of the same filter from one sealed storage until a write lands in its - * set. Several slots, because NIP-FE's stateless rounds re-open per round and a relay syncs more - * than one filter at a time; rebuilding costs a full scan + O(n log n) seal that grows with the - * corpus: relayBench measured 342 ms per identical-set reconcile at 50k events vs strfry's 26 ms - * off its always-current tree. + * set. Several slots, because a relay reconciles more than one filter at a time and a busy one + * takes writes between every open; rebuilding costs a full scan + O(n log n) seal that grows with + * the corpus: relayBench measured 342 ms per identical-set reconcile at 50k events vs strfry's + * 26 ms off its always-current tree. */ override suspend fun sealedNegentropyStorage( filters: List<Filter>, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayCommand.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayCommand.kt index 6d48801955..2716cba3c6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayCommand.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayCommand.kt @@ -29,18 +29,11 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CountCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd -import com.vitorpamplona.quartz.nip77Negentropy.NegErrMessage -import com.vitorpamplona.quartz.nip77Negentropy.NegMsgMessage -import com.vitorpamplona.quartz.nip77Negentropy.NegOpenCmd /** * NIP-FE: the client commands HTTP carries, one path each. A body is the command's arguments * after its subscription id (a lone object where the command takes one); the answer ends on the * first frame [ends] accepts. - * - * `NEG` is not in NIP-FE; it is this implementation's extension: one NIP-77 round, - * `[filter, message]`. The responder keeps no state between rounds but its snapshot, which the - * backend caches per filter, so each round carries its filter and there is no session to close. */ enum class HttpRelayCommand( val path: String, @@ -48,7 +41,6 @@ enum class HttpRelayCommand( REQ("/req"), COUNT("/count"), EVENT("/event"), - NEG("/neg"), ; /** @@ -74,11 +66,6 @@ enum class HttpRelayCommand( if (!shape.isObject) return null "[\"${EventCmd.LABEL}\",${shape.text}]" } - - NEG -> { - if (shape.isObject || shape.elements != NEG_ROUND) return null - frame(NegOpenCmd.LABEL, SUB_ID, shape.inner) - } } } @@ -89,7 +76,6 @@ enum class HttpRelayCommand( REQ -> message is EoseMessage || message is ClosedMessage COUNT -> message is CountMessage || message is ClosedMessage EVENT -> message is OkMessage - NEG -> message is NegMsgMessage || message is NegErrMessage } companion object { @@ -99,8 +85,6 @@ enum class HttpRelayCommand( */ const val SUB_ID = "http" - private val NEG_ROUND = listOf('{', '"') - fun forPath(path: String): HttpRelayCommand? = entries.firstOrNull { it.path == path } private fun frame( diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt index e443028888..ef4056423d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt @@ -346,7 +346,7 @@ class HttpRelayHandler( } /** The frames that carry a subscription id in the engine; NIP-FE sends them without it. */ -private val SUBSCRIPTION_FRAMES = setOf("EVENT", "EOSE", "CLOSED", "COUNT", "NEG-MSG", "NEG-ERR") +private val SUBSCRIPTION_FRAMES = setOf("EVENT", "EOSE", "CLOSED", "COUNT") private const val SUB_ID_FIELD = ",\"" + HttpRelayCommand.SUB_ID + "\"" diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayStatus.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayStatus.kt index 496acab463..370ca0395e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayStatus.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayStatus.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NoticeMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage import com.vitorpamplona.quartz.nip01Core.store.RejectionReason -import com.vitorpamplona.quartz.nip77Negentropy.NegErrMessage /** * NIP-FE status codes. The status of an answer is decided by its first frame: an accepting one @@ -47,7 +46,6 @@ object HttpRelayStatus { fun of(message: Message?): Int = when (message) { is ClosedMessage -> forReason(message.message) - is NegErrMessage -> forReason(message.reason) // A duplicate is already stored, which is what the caller asked for, whichever flag the store set. is OkMessage -> if (message.success || message.message.startsWith(RejectionReason.PREFIX_DUPLICATE)) OK else forReason(message.message) is NoticeMessage -> BAD_REQUEST diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt index 4123405322..3f39895789 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt @@ -20,11 +20,8 @@ */ package com.vitorpamplona.quartz.nipFERelayOverHttp -import com.vitorpamplona.negentropy.Negentropy -import com.vitorpamplona.negentropy.storage.StorageVector import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.MachineReadablePrefix import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd @@ -43,7 +40,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.server.policies.RelayLimits import com.vitorpamplona.quartz.nip01Core.relay.server.policies.VerifyPolicy import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.store.IEventStore -import com.vitorpamplona.quartz.nip01Core.store.IdAndTime import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip77Negentropy.NegentropySettings import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent @@ -60,7 +56,7 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds -/** NIP-FE's handler over a relay engine and an in-memory backend: status, lines, auth, and negentropy rounds. */ +/** NIP-FE's handler over a relay engine and an in-memory backend: status, lines and auth. */ class HttpRelayHandlerTest { /** Everything the backend holds; a filter naming [STALLED_KIND] never answers, one naming [TRICKLE_KIND] never ends. */ private class MemoryBackend : SessionBackend { @@ -91,11 +87,6 @@ class HttpRelayHandlerTest { if (events.none { it.id == event.id }) events += event onComplete(IEventStore.InsertOutcome.Accepted) } - - override suspend fun snapshotIdsForNegentropy( - filters: List<Filter>, - maxEntries: Int?, - ) = events.filter { e -> filters.any { it.match(e) } }.map { IdAndTime(it.createdAt, it.id) } } /** Refuses every read from a connection nobody signed in on, as an AUTH-gated relay does. */ @@ -238,7 +229,6 @@ class HttpRelayHandlerTest { for ((command, body) in listOf( HttpRelayCommand.REQ to "[]", HttpRelayCommand.EVENT to """[{"id":"x"}]""", - HttpRelayCommand.NEG to """[{"kinds":[1]}]""", HttpRelayCommand.COUNT to "not json", )) { val answer = handler().ask(command, body) @@ -285,45 +275,6 @@ class HttpRelayHandlerTest { assertTrue("payload" in other.lines.single(), other.lines.toString()) } - @Test - fun negentropyReconcilesInStatelessRounds() { - val shared = (1..30).map { note("shared $it", 1_700_000_000L + it) } - val onlyRelay = (1..12).map { note("relay $it", 1_700_001_000L + it) } - val onlyClient = (1..7).map { note("client $it", 1_700_002_000L + it) } - backend.events += shared + onlyRelay - - val mine = StorageVector().apply { (shared + onlyClient).forEach { insert(it.createdAt, it.id) } }.also { it.seal() } - val client = Negentropy(mine, 0) - var message = client.initiate().toHexKey() - val have = mutableSetOf<String>() - val need = mutableSetOf<String>() - var rounds = 0 - while (true) { - check(++rounds < 20) { "no convergence" } - // A fresh handler each round: nothing on the server side carries over. - val answer = handler().ask(HttpRelayCommand.NEG, """[{"kinds":[1]},"$message"]""") - assertEquals(200, answer.status, answer.lines.toString()) - val reply = - answer.lines - .single() - .substringAfter("""["NEG-MSG","""") - .substringBefore('"') - val result = client.reconcile(reply.hexToByteArray()) - have += result.sendIds.map { it.toHexString() } - need += result.needIds.map { it.toHexString() } - message = result.msg?.toHexKey() ?: break - } - assertEquals(onlyClient.map { it.id }.toSet(), have) - assertEquals(onlyRelay.map { it.id }.toSet(), need) - } - - @Test - fun aMalformedNegentropyRoundIsRefused() { - val answer = handler().ask(HttpRelayCommand.NEG, """[{"kinds":[1]},"zz"]""") - assertEquals(400, answer.status) - assertTrue(answer.lines.single().startsWith("""["NEG-ERR","""), answer.lines.toString()) - } - @Test fun noFirstFrameWithinTheDeadlineIsA503() { val answer = handler(deadline = 300.milliseconds).ask(HttpRelayCommand.REQ, """{"kinds":[$STALLED_KIND]}""") @@ -451,13 +402,12 @@ class HttpRelayHandlerTest { fun aBackendFailureIsA500Line() { val failing = object : SessionBackend by backend { - override suspend fun sealedNegentropyStorage( - filters: List<Filter>, - maxEntries: Int, - ) = error("db is down") + override suspend fun submit( + event: Event, + onComplete: (IEventStore.InsertOutcome) -> Unit, + ): Unit = error("db is down") } - val message = Negentropy(StorageVector().also { it.seal() }, 0).initiate().toHexKey() - val answer = HttpRelayHandler(MemoryRelay(failing, { VerifyPolicy }), origins = { listOf(origin) }).ask(HttpRelayCommand.NEG, """[{"kinds":[1]},"$message"]""") + val answer = HttpRelayHandler(MemoryRelay(failing, { VerifyPolicy }), origins = { listOf(origin) }).ask(HttpRelayCommand.EVENT, note("lost").toJson()) assertEquals(500, answer.status, answer.lines.toString()) assertTrue(answer.lines.single().startsWith("""["CLOSED","error:"""), answer.lines.toString()) } @@ -491,14 +441,10 @@ class HttpRelayHandlerTest { fun theBodyShapeIsReadWithoutATree() { assertEquals("""["REQ","http",{"kinds":[1]}]""", HttpRelayCommand.REQ.frameOf(""" {"kinds":[1]} """)) assertEquals("""["COUNT","http",{"a":"]"},{"b":"\"["}]""", HttpRelayCommand.COUNT.frameOf("""[{"a":"]"},{"b":"\"["}]""")) - assertEquals("""["NEG-OPEN","http",{"kinds":[1]},"61"]""", HttpRelayCommand.NEG.frameOf("""[{"kinds":[1]},"61"]""")) assertEquals("""["EVENT",{"id":"x"}]""", HttpRelayCommand.EVENT.frameOf("""{"id":"x"}""")) for (bad in listOf("", "[]", "{", "[{}", "{}}", "{]", "[{}]x", "{} {}", "[{},]", "[,{}]", "[{} {}]", "[1]", """[{},"x"]""", "null", "\"x\"")) { assertEquals(null, HttpRelayCommand.REQ.frameOf(bad), "REQ '$bad'") } - for (bad in listOf("""[{"kinds":[1]}]""", """["61",{}]""", """[{},61]""", """[{},"61",1]""", """{"kinds":[1]}""")) { - assertEquals(null, HttpRelayCommand.NEG.frameOf(bad), "NEG '$bad'") - } for (bad in listOf("""[{"id":"x"}]""", "1")) assertEquals(null, HttpRelayCommand.EVENT.frameOf(bad), "EVENT '$bad'") } From 90e346a0e54b9f86bc5d7122101f6ddca002f3e2 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sun, 27 Sep 2026 14:00:14 +0000 Subject: [PATCH 51/60] fix(test): resolve the playback-error description outside composition be7be5b6 dropped the Context argument from the description lookup in PlaybackErrorOverlayFitTest, which turned it into a call to the @Composable stringRes overload from a plain test function, so the androidTest source set stopped compiling. Load it with loadStringRes in runBlocking, like the test's other lookups, and drop the now-unused targetContext. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkG4KTqmnprUK9BAcwAyc8 --- .../composable/PlaybackErrorOverlayFitTest.kt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/service/playback/composable/PlaybackErrorOverlayFitTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/service/playback/composable/PlaybackErrorOverlayFitTest.kt index ae3095d27e..1b86d61661 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/service/playback/composable/PlaybackErrorOverlayFitTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/service/playback/composable/PlaybackErrorOverlayFitTest.kt @@ -39,13 +39,11 @@ import androidx.compose.ui.unit.dp import androidx.media3.common.PlaybackException import androidx.media3.common.Player import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.error_video_open_in_browser import com.vitorpamplona.amethyst.commons.resources.error_video_playback_failed import com.vitorpamplona.amethyst.commons.resources.error_video_playback_failed_description import com.vitorpamplona.amethyst.commons.ui.loadStringRes -import com.vitorpamplona.amethyst.commons.ui.stringRes import io.mockk.mockk import kotlinx.coroutines.runBlocking import org.junit.Assert.assertTrue @@ -70,8 +68,6 @@ class PlaybackErrorOverlayFitTest { @get:Rule val rule = createComposeRule() - private val targetContext = InstrumentationRegistry.getInstrumentation().targetContext - /** * Built outside composition on purpose: the mock and its error state are fixtures for the whole * test, not per-composition state. Creating them inside `setContent` would rebuild both on every @@ -141,10 +137,12 @@ class PlaybackErrorOverlayFitTest { assertButtonUsable() rule .onNodeWithText( - stringRes( - Res.string.error_video_playback_failed_description, - "ERROR_CODE_PARSING_MANIFEST_MALFORMED", - ), + runBlocking { + loadStringRes( + Res.string.error_video_playback_failed_description, + "ERROR_CODE_PARSING_MANIFEST_MALFORMED", + ) + }, ).assertDoesNotExist() } From 71d0c25b6430e4342bea6c235a05d7bf6262e4cd Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sun, 27 Sep 2026 14:00:47 +0000 Subject: [PATCH 52/60] Drop the per-filter NIP-77 snapshot cache from this PR It was here for stateless /neg rounds, which rebuilt a snapshot per round. With /neg removed it only speeds up NEG-OPEN on the websocket, which is out of scope for a NIP-FE follow-up. LiveEventStore is back to main's single-slot cache. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jDNhr6a3J4VC5TaG3Yd48 --- .../relay/server/backend/LiveEventStore.kt | 86 ++++------- .../LiveEventStoreSnapshotCacheTest.kt | 133 ------------------ 2 files changed, 30 insertions(+), 189 deletions(-) delete mode 100644 quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/LiveEventStoreSnapshotCacheTest.kt diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/LiveEventStore.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/LiveEventStore.kt index 88da1daaed..2c37a5b904 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/LiveEventStore.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/LiveEventStore.kt @@ -22,21 +22,18 @@ package com.vitorpamplona.quartz.nip01Core.relay.server.backend import com.vitorpamplona.negentropy.storage.IStorage import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.core.isAddressable -import com.vitorpamplona.quartz.nip01Core.core.isReplaceable import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterIndex import com.vitorpamplona.quartz.nip01Core.store.IEventStore import com.vitorpamplona.quartz.nip01Core.store.IdAndTime import com.vitorpamplona.quartz.nip01Core.store.RawEvent import com.vitorpamplona.quartz.nip01Core.store.StoreQueryContext -import com.vitorpamplona.quartz.nip09Deletions.DeletionRequestEvent -import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.withContext import kotlin.concurrent.atomics.AtomicBoolean +import kotlin.concurrent.atomics.AtomicLong import kotlin.concurrent.atomics.AtomicReference import kotlin.concurrent.atomics.ExperimentalAtomicApi @@ -157,7 +154,7 @@ class LiveEventStore( ) { ingest.submit(event, skipVerify) { outcome -> if (outcome is IEventStore.InsertOutcome.Accepted) { - forgetSnapshotsCovering(event) + writeGeneration.addAndFetch(1L) fanout(event) } onComplete(outcome) @@ -391,28 +388,32 @@ class LiveEventStore( // ------------------------------------------------------------------ /** - * The last few sealed negentropy snapshots, most recently used first, each valid until a write - * could change its set. Replaced whole on every change, never mutated, so a reader never sees a - * half-updated list. Deletion paths that bypass the ingest queue (expiration sweeps, NIP-86 admin - * purges) are not seen here, which is why entries also carry a short TTL: a snapshot is a - * point-in-time set by NIP-77's nature, and a few seconds of staleness only means a peer - * momentarily re-offers ids the relay just dropped. + * Bumped after every accepted write. A cached negentropy snapshot is + * only valid while this hasn't moved. Deletion paths that bypass the + * ingest queue (expiration sweeps, NIP-86 admin purges) don't bump it, + * which is why cache entries also carry a short TTL: a snapshot is a + * point-in-time set by NIP-77's nature, and a few seconds of staleness + * only means a peer momentarily re-offers ids the relay just dropped. */ - private val snapshotCache = AtomicReference<List<CachedSnapshot>>(emptyList()) + private val writeGeneration = AtomicLong(0L) private class CachedSnapshot( val filterKey: String, - val filters: List<Filter>, + val generation: Long, val builtAt: Long, val storage: IStorage?, ) + private val snapshotCache = AtomicReference<CachedSnapshot?>(null) + /** - * Serves repeated NEG-OPENs of the same filter from one sealed storage until a write lands in its - * set. Several slots, because a relay reconciles more than one filter at a time and a busy one - * takes writes between every open; rebuilding costs a full scan + O(n log n) seal that grows with - * the corpus: relayBench measured 342 ms per identical-set reconcile at 50k events vs strfry's - * 26 ms off its always-current tree. + * Serves repeated NEG-OPENs of the same filter from one sealed + * storage as long as no write landed in between (single slot — the + * mirror-heartbeat pattern is many peers reconciling the same broad + * filter, not many filters). Rebuilding on every open costs a full + * scan + O(n log n) seal that grows with the corpus: relayBench + * measured 342 ms per identical-set reconcile at 50k events vs + * strfry's 26 ms off its always-current tree. */ override suspend fun sealedNegentropyStorage( filters: List<Filter>, @@ -427,57 +428,30 @@ class LiveEventStore( store.liveNegentropySnapshot(maxEntries)?.let { return it } } - val key = filters.joinToString(" ") { it.toJson() } + " cap=$maxEntries" + val generation = writeGeneration.load() + val key = filters.joinToString("�") { it.toJson() } + "�cap=$maxEntries" val now = TimeUtils.now() - snapshotCache.load().firstOrNull { it.filterKey == key && now - it.builtAt <= SNAPSHOT_TTL_SECONDS }?.let { hit -> - updateSnapshots { cached -> listOf(hit) + cached.filter { it !== hit } } - return hit.storage + val cached = snapshotCache.load() + if (cached != null && + cached.filterKey == key && + cached.generation == generation && + now - cached.builtAt <= SNAPSHOT_TTL_SECONDS + ) { + return cached.storage } val built = super.sealedNegentropyStorage(filters, maxEntries) - val entry = CachedSnapshot(key, filters, now, built) - updateSnapshots { cached -> (listOf(entry) + cached.filter { it.filterKey != key }).take(SNAPSHOT_SLOTS) } + snapshotCache.store(CachedSnapshot(key, generation, now, built)) return built } - /** - * Drops every cached snapshot whose set [event] can change: one of its filters admits the event, - * or the event can remove members it cannot see — a deletion or vanish request clears them all, - * and a replaceable or addressable event drops any snapshot whose kinds and authors it falls in, - * since the version it replaces may be in the set under tags or ids the new one no longer has. - */ - private fun forgetSnapshotsCovering(event: Event) { - if (snapshotCache.load().isEmpty()) return - if (event.kind == DeletionRequestEvent.KIND || event.kind == RequestToVanishEvent.KIND) { - updateSnapshots { emptyList() } - return - } - val supersedes = event.kind.isReplaceable() || event.kind.isAddressable() - updateSnapshots { cached -> - cached.filter { snapshot -> - snapshot.filters.none { f -> f.match(event) || (supersedes && Filter(kinds = f.kinds, authors = f.authors).match(event)) } - } - } - } - - private inline fun updateSnapshots(change: (List<CachedSnapshot>) -> List<CachedSnapshot>) { - while (true) { - val current = snapshotCache.load() - val next = change(current) - if (next == current || snapshotCache.compareAndSet(current, next)) return - } - } - private companion object { /** * Ceiling on how long a cached snapshot may serve NEG-OPENs even * with no observed writes — bounds staleness from delete paths - * the ingest queue never sees. + * the generation counter can't see. */ const val SNAPSHOT_TTL_SECONDS = 30L - - /** Distinct filters kept sealed at once; each holds up to maxSyncEvents ids at ~40 B each. */ - const val SNAPSHOT_SLOTS = 4 } } diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/LiveEventStoreSnapshotCacheTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/LiveEventStoreSnapshotCacheTest.kt deleted file mode 100644 index eacc390d3f..0000000000 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/LiveEventStoreSnapshotCacheTest.kt +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip01Core.relay.server.backend - -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync -import com.vitorpamplona.quartz.nip01Core.store.IEventStore -import com.vitorpamplona.quartz.nip01Core.store.IdAndTime -import com.vitorpamplona.quartz.nip01Core.store.sqlite.DefaultIndexingStrategy -import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventStore -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.runBlocking -import kotlin.test.AfterTest -import kotlin.test.Test -import kotlin.test.assertEquals - -/** The NIP-77 snapshot cache: kept across writes that cannot change its set, dropped by those that can. */ -class LiveEventStoreSnapshotCacheTest { - /** Counts how often a snapshot is actually scanned out of the store. */ - private class Counting( - private val inner: IEventStore, - ) : IEventStore by inner { - var scans = 0 - - override suspend fun snapshotIdsForNegentropy( - filters: List<Filter>, - maxEntries: Int?, - onProgress: ((collected: Int) -> Unit)?, - ): List<IdAndTime> { - scans++ - return inner.snapshotIdsForNegentropy(filters, maxEntries, onProgress) - } - } - - private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) - private val store = Counting(EventStore(dbName = null, indexStrategy = DefaultIndexingStrategy(indexEventsByPubkeyAlone = true))) - private val live = LiveEventStore(store, IngestQueue(store, scope.coroutineContext)) - private val alice = NostrSignerSync() - private var clock = 1_700_000_000L - - private val notes = listOf(Filter(kinds = listOf(1))) - private val reactions = listOf(Filter(kinds = listOf(7))) - - @AfterTest - fun tearDown() { - scope.cancel() - } - - private fun event( - kind: Int, - tags: Array<Array<String>> = emptyArray(), - ) = alice.sign<Event>(clock++, kind, tags, "") - - private fun publish(event: Event) = - runBlocking { - val done = CompletableDeferred<IEventStore.InsertOutcome>() - live.submit(event) { done.complete(it) } - assertEquals(IEventStore.InsertOutcome.Accepted, done.await()) - } - - private fun open(filters: List<Filter>) = runBlocking { live.sealedNegentropyStorage(filters, maxEntries = 1_000) } - - @Test - fun twoFiltersInTurnAreEachScannedOnce() { - publish(event(1)) - repeat(3) { - open(notes) - open(reactions) - } - assertEquals(2, store.scans) - } - - @Test - fun aWriteOutsideTheSetKeepsTheSnapshot() { - open(notes) - publish(event(7)) - open(notes) - assertEquals(1, store.scans) - } - - @Test - fun aWriteInsideTheSetRebuildsIt() { - open(notes) - publish(event(1)) - open(notes) - assertEquals(2, store.scans) - } - - @Test - fun aDeletionDropsEverySnapshot() { - open(notes) - open(reactions) - publish(event(5, arrayOf(arrayOf("e", "a".repeat(64))))) - open(notes) - open(reactions) - assertEquals(4, store.scans) - } - - @Test - fun aReplacementDropsASnapshotItsOldVersionWasIn() { - // The stored profile is in the set by its tag; its replacement has no such tag and so does not - // match the filter, but it removes the old version from the set all the same. - val tagged = listOf(Filter(kinds = listOf(0), tags = mapOf("t" to listOf("nostr")))) - publish(event(0, arrayOf(arrayOf("t", "nostr")))) - open(tagged) - publish(event(0)) - open(tagged) - assertEquals(2, store.scans) - } -} From 4e6472cd09decf9cfda2c5e3790e21fcc439009b Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sun, 27 Sep 2026 14:10:14 +0000 Subject: [PATCH 53/60] NIP-FE: NIP-98 tokens are not single-use Nip98AuthVerifier gains rejectReplays (default true, so NIP-86 keeps it). The NIP-FE handler turns it off: a request may land on any instance, which a per-process memory of spent tokens cannot follow, and the body's hash already limits a captured token to repeating the one command it signs inside its window. Verifier tests cover single use, a full cache, and a token without replay rejection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jDNhr6a3J4VC5TaG3Yd48 --- .../quartz/nip98HttpAuth/Nip98AuthVerifier.kt | 9 +++++ .../nipFERelayOverHttp/HttpRelayHandler.kt | 15 ++++---- .../nip98HttpAuth/Nip98AuthVerifierTest.kt | 36 +++++++++++++++++++ .../HttpRelayHandlerTest.kt | 12 +++---- 4 files changed, 58 insertions(+), 14 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifier.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifier.kt index da83ca0d6d..7bef59da73 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifier.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifier.kt @@ -44,6 +44,7 @@ import kotlin.math.abs * 4. The `method` tag matches the HTTP method. * 5. The `u` tag matches the requested URL. * 6. If a body is present, the `payload` tag matches `sha256(body)` hex. + * 7. With [rejectReplays], the token has not been accepted before. * * Returns the verified pubkey on success; a [Result.Malformed] / * [Result.Missing] otherwise (the caller turns these into 401/403). @@ -58,6 +59,12 @@ class Nip98AuthVerifier( * captured token be replayed. Size it for the endpoint's signed-request rate over 2 x tolerance. */ private val maxReplayEntries: Int = MAX_REPLAY_ENTRIES, + /** + * Accept each token once. NIP-98 does not ask for it, and the memory is this process's alone, so + * behind a load balancer it holds per instance. Off, a token captured in its window can repeat + * the request it signs, and only that request when it binds the body's hash. + */ + private val rejectReplays: Boolean = true, ) { /** * Recently-accepted event ids → expiry epoch second. Bounded to @@ -133,6 +140,8 @@ class Nip98AuthVerifier( } } + if (!rejectReplays) return Result.Verified(event.pubKey) + // Replay check — done LAST so we don't burn a one-shot id on a // request that would otherwise have failed signature/url/etc. val expiry = nowSec + 2 * toleranceSeconds diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt index ef4056423d..fbe22d3468 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt @@ -99,8 +99,12 @@ class HttpRelayHandler( private val origins: () -> List<String>, /** How long one answer may run, first byte to last. [Duration.INFINITE] turns the deadline off. */ private val deadline: Duration = DEFAULT_DEADLINE, - /** Its own replay cache, sized for a public endpoint, so it cannot be flushed to replay a token. */ - private val verifier: Nip98AuthVerifier = Nip98AuthVerifier(maxReplayEntries = DEFAULT_REPLAY_ENTRIES), + /** + * Tokens are not single-use: a request can land on any instance, which a per-process memory of + * spent tokens cannot follow, and the body's hash already limits a captured token to the one + * command it signs, inside its window. + */ + private val verifier: Nip98AuthVerifier = Nip98AuthVerifier(rejectReplays = false), /** Frames queued ahead of a slow reader before the answer is cut short. */ private val maxQueuedFrames: Int = DEFAULT_MAX_QUEUED_FRAMES, /** How long past the deadline the last line may take before the reader counts as stalled. */ @@ -291,7 +295,7 @@ class HttpRelayHandler( /** * A NIP-98 header, checked against the address it names when that is one of [origins], so a token - * signed at the .onion verifies there. It must bind the body's hash: it authorizes one command, once. + * signed at the .onion verifies there. It must bind the body's hash: it authorizes one command. * Another scheme (a proxy's Basic, a client's Bearer) is not addressed to the relay and is ignored. */ private suspend fun proofOf(request: HttpRelayRequest): Proof { @@ -310,7 +314,7 @@ class HttpRelayHandler( Proof.Anonymous } - // A full replay cache is the relay's limit, not the token's fault. + // A full replay cache, in a verifier that keeps one, is the relay's limit, not the token's fault. is Nip98AuthVerifier.Result.Malformed -> { if (MachineReadablePrefix.parse(r.reason) == MachineReadablePrefix.RATE_LIMITED) { Proof.Refused(r.reason) @@ -339,9 +343,6 @@ class HttpRelayHandler( const val DEFAULT_MAX_QUEUED_FRAMES = 8192 val DEFAULT_TAIL_GRACE = 5_000.milliseconds - - /** Two minutes of tokens (the replay window) at about 500 signed commands a second. */ - const val DEFAULT_REPLAY_ENTRIES = 65_536 } } diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifierTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifierTest.kt index 2323410d5b..d92e411946 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifierTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifierTest.kt @@ -131,4 +131,40 @@ class Nip98AuthVerifierTest { assertTrue(r.reason.contains("kind")) } } + + @Test + fun aTokenIsAcceptedOnce() { + runBlocking { + val body = "hello".encodeToByteArray() + val (_, header) = signedToken("http://x/", "POST", body) + assertIs<Nip98AuthVerifier.Result.Verified>(verifier.verify(header, "POST", "http://x/", body)) + val again = verifier.verify(header, "POST", "http://x/", body) + assertIs<Nip98AuthVerifier.Result.Malformed>(again) + assertTrue(again.reason.contains("replay")) + } + } + + @Test + fun aFullCacheRefusesTheNewTokenAndStillRemembersTheOld() { + runBlocking { + val small = Nip98AuthVerifier(now = { 1_000L }, maxReplayEntries = 2) + val tokens = (0..2).map { n -> "body $n".encodeToByteArray().let { it to signedToken("http://x/", "POST", it).second } } + for ((body, header) in tokens.take(2)) assertIs<Nip98AuthVerifier.Result.Verified>(small.verify(header, "POST", "http://x/", body)) + val (body, header) = tokens[2] + assertEquals(Nip98AuthVerifier.Result.Malformed(Nip98AuthVerifier.REPLAY_CACHE_FULL), small.verify(header, "POST", "http://x/", body)) + val (oldBody, oldHeader) = tokens[0] + assertTrue((small.verify(oldHeader, "POST", "http://x/", oldBody) as Nip98AuthVerifier.Result.Malformed).reason.contains("replay")) + } + } + + @Test + fun withoutReplayRejectionATokenVerifiesAgainButOnlyForItsBody() { + runBlocking { + val reusable = Nip98AuthVerifier(now = { 1_000L }, rejectReplays = false) + val body = "hello".encodeToByteArray() + val (_, header) = signedToken("http://x/", "POST", body) + repeat(3) { assertIs<Nip98AuthVerifier.Result.Verified>(reusable.verify(header, "POST", "http://x/", body)) } + assertIs<Nip98AuthVerifier.Result.Malformed>(reusable.verify(header, "POST", "http://x/", "other".encodeToByteArray())) + } + } } diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt index 3f39895789..461012547d 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt @@ -262,14 +262,12 @@ class HttpRelayHandlerTest { } @Test - fun aTokenForAnotherBodyOrASecondUseIsRefused() { + fun aTokenSignsOnlyItsBodyAndIsNotSingleUse() { val h = handler() val body = """{"kinds":[1]}""" - val once = token(HttpRelayCommand.REQ, body) - assertEquals(200, h.ask(HttpRelayCommand.REQ, body, once).status) - val replayed = h.ask(HttpRelayCommand.REQ, body, once) - assertEquals(401, replayed.status) - assertTrue("replay" in replayed.lines.single(), replayed.lines.toString()) + val signed = token(HttpRelayCommand.REQ, body) + assertEquals(200, h.ask(HttpRelayCommand.REQ, body, signed).status) + assertEquals(200, h.ask(HttpRelayCommand.REQ, body, signed).status, "any instance may answer it, so none remembers it") val other = h.ask(HttpRelayCommand.REQ, """{"kinds":[0]}""", token(HttpRelayCommand.REQ, body)) assertEquals(401, other.status) assertTrue("payload" in other.lines.single(), other.lines.toString()) @@ -367,7 +365,7 @@ class HttpRelayHandlerTest { } @Test - fun aFloodOfFreshTokensCannotFlushAReplay() { + fun aHostsSingleUseVerifierThatIsFullIsA429() { val h = HttpRelayHandler(MemoryRelay(backend, false), origins = { listOf(origin) }, verifier = Nip98AuthVerifier(maxReplayEntries = 4)) // A token's id is its hash, so tokens signed the same second over the same body are one token: From 2a23450afc3d1d3c3bfc7debba54eb45edd4caba Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sun, 27 Sep 2026 14:16:44 +0000 Subject: [PATCH 54/60] NIP-FE: splice the body and let the engine parse it; verify NIP-98 against every origin JsonShape is gone. The body goes into its frame as sent and the engine parses it as it parses socket text, through the same mapper: the verb and subscription id come first and one value is read, so a body cannot become another command, and a malformed one is the engine's NOTICE. Jackson's nesting limit already bounds depth, as it does on the socket. Nip98AuthVerifier.verify takes the accepted URLs, so the handler no longer decodes the token a second time to pick one (claimedUrl). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jDNhr6a3J4VC5TaG3Yd48 --- .../quartz/nip98HttpAuth/Nip98AuthVerifier.kt | 16 ++- .../nipFERelayOverHttp/HttpRelayCommand.kt | 111 ++---------------- .../nipFERelayOverHttp/HttpRelayHandler.kt | 21 +--- .../HttpRelayHandlerTest.kt | 26 +++- 4 files changed, 48 insertions(+), 126 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifier.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifier.kt index 7bef59da73..06d0ce31cd 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifier.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifier.kt @@ -42,7 +42,8 @@ import kotlin.math.abs * 2. Decoded body is a kind-27235 event with a valid Schnorr signature. * 3. The event's `created_at` is within ±[toleranceSeconds] of now. * 4. The `method` tag matches the HTTP method. - * 5. The `u` tag matches the requested URL. + * 5. The `u` tag is the requested URL, or one of them: a server reachable at more than one + * address (a clearnet host and an .onion) accepts a token signed at any. * 6. If a body is present, the `payload` tag matches `sha256(body)` hex. * 7. With [rejectReplays], the token has not been accepted before. * @@ -80,12 +81,19 @@ class Nip98AuthVerifier( private val seenLock = Mutex() - @OptIn(ExperimentalEncodingApi::class) suspend fun verify( authorizationHeader: String?, method: String, url: String, body: ByteArray?, + ): Result = verify(authorizationHeader, method, listOf(url), body) + + @OptIn(ExperimentalEncodingApi::class) + suspend fun verify( + authorizationHeader: String?, + method: String, + urls: Collection<String>, + body: ByteArray?, ): Result { if (authorizationHeader.isNullOrBlank()) return Result.Missing if (!authorizationHeader.startsWith(SCHEME)) return Result.Malformed("expected '$SCHEME <base64>' header") @@ -130,8 +138,8 @@ class Nip98AuthVerifier( if (!auth.method().equals(method, ignoreCase = true)) { return Result.Malformed("method mismatch: expected $method, got ${auth.method()}") } - if (auth.url() != url) { - return Result.Malformed("url mismatch: expected $url, got ${auth.url()}") + if (auth.url() !in urls) { + return Result.Malformed("url mismatch: expected ${urls.joinToString(" or ")}, got ${auth.url()}") } if (body != null && body.isNotEmpty()) { val expected = sha256(body).toHexKey() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayCommand.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayCommand.kt index 2716cba3c6..cb5a865ea6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayCommand.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayCommand.kt @@ -44,27 +44,27 @@ enum class HttpRelayCommand( ; /** - * The client frame [body] stands for, or null when it is not this command's arguments. Only - * the body's outer shape is checked here ([JsonShape]); the engine parses the frame once, so a - * malformed inside is its usual NOTICE. The shape check is what makes splicing safe: the body - * is one balanced value with nothing after it, so it cannot close the frame or open another. + * The client frame [body] stands for, or null when it plainly is not this command's arguments. + * The body is spliced in as sent and the engine parses the frame, as it parses socket text, so + * any other malformed body is the engine's NOTICE. The verb and subscription id come first and + * the parser reads one value, so nothing a body holds can make it another command. */ fun frameOf(body: String): String? { - val shape = JsonShape.of(body) ?: return null + val text = body.trim() return when (this) { REQ, COUNT -> { val filters = when { - shape.isObject -> shape.text - shape.elements.isNotEmpty() && shape.elements.all { it == '{' } -> shape.inner + text.startsWith('{') -> text + text.startsWith('[') && text.endsWith(']') -> text.substring(1, text.length - 1).trim().ifEmpty { return null } else -> return null } - frame(if (this == REQ) ReqCmd.LABEL else CountCmd.LABEL, SUB_ID, filters) + "[\"${if (this == REQ) ReqCmd.LABEL else CountCmd.LABEL}\",\"$SUB_ID\",$filters]" } EVENT -> { - if (!shape.isObject) return null - "[\"${EventCmd.LABEL}\",${shape.text}]" + if (!text.startsWith('{')) return null + "[\"${EventCmd.LABEL}\",$text]" } } } @@ -86,96 +86,5 @@ enum class HttpRelayCommand( const val SUB_ID = "http" fun forPath(path: String): HttpRelayCommand? = entries.firstOrNull { it.path == path } - - private fun frame( - label: String, - subId: String, - args: String, - ) = "[\"$label\",\"$subId\",$args]" - } -} - -/** - * The outer shape of a JSON body, read without building a tree: one object or array, brackets - * matched by type outside strings, nesting no deeper than [MAX_DEPTH], nothing after it. An array's - * [elements] are each top-level element's first character. Everything inside is left to the parser. - */ -internal class JsonShape private constructor( - val text: String, - val isObject: Boolean, - val elements: List<Char>, -) { - /** An array's contents without its brackets. */ - val inner: String get() = text.substring(1, text.length - 1) - - companion object { - /** Deep enough for any filter, event or round by a wide margin; far too shallow to exhaust a stack. */ - const val MAX_DEPTH = 32 - - fun of(body: String): JsonShape? { - val text = body.trim() - if (text.length < 2 || (text[0] != '{' && text[0] != '[')) return null - val elements = ArrayList<Char>() - val open = CharArray(MAX_DEPTH) - var depth = 0 - var inString = false - var escaped = false - // At depth 1 inside an array: whether the next non-space character starts an element. - var expectElement = text[0] == '[' - var i = 0 - while (i < text.length) { - val c = text[i] - if (inString) { - when { - escaped -> escaped = false - c == '\\' -> escaped = true - c == '"' -> inString = false - } - i++ - continue - } - if (depth == 0 && i > 0) return null - if (depth == 1 && text[0] == '[' && !c.isWhitespace()) { - when { - c == ',' -> { - if (expectElement) return null - expectElement = true - i++ - continue - } - - c == ']' -> { - if (expectElement && elements.isNotEmpty()) return null - } - - expectElement -> { - elements.add(c) - expectElement = false - } - - c == '{' || c == '[' || c == '"' -> { - return null - } - } - } - when (c) { - '"' -> { - inString = true - } - - '{', '[' -> { - if (depth == MAX_DEPTH) return null - open[depth++] = c - } - - '}', ']' -> { - if (depth == 0 || open[--depth] != (if (c == '}') '{' else '[')) return null - } - } - i++ - } - if (depth != 0 || inString) return null - return JsonShape(text, text[0] == '{', elements) - } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt index fbe22d3468..90b611720e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt @@ -21,14 +21,12 @@ package com.vitorpamplona.quartz.nipFERelayOverHttp import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.AuthMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ClosedMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.MachineReadablePrefix import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.nip01Core.relay.server.RelayServerBase import com.vitorpamplona.quartz.nip01Core.relay.server.SessionSink -import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent import com.vitorpamplona.quartz.nip98HttpAuth.Nip98AuthVerifier import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred @@ -38,8 +36,6 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeoutOrNull -import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.TimeSource @@ -294,8 +290,8 @@ class HttpRelayHandler( } /** - * A NIP-98 header, checked against the address it names when that is one of [origins], so a token - * signed at the .onion verifies there. It must bind the body's hash: it authorizes one command. + * A NIP-98 header, checked against every address in [origins], so a token signed at the .onion + * verifies there. It must bind the body's hash: it authorizes one command. * Another scheme (a proxy's Basic, a client's Bearer) is not addressed to the relay and is ignored. */ private suspend fun proofOf(request: HttpRelayRequest): Proof { @@ -304,8 +300,8 @@ class HttpRelayHandler( if (!header.regionMatches(0, scheme, 0, scheme.length, ignoreCase = true)) return Proof.Anonymous val token = scheme + header.substring(scheme.length).trim() val accepted = origins().map { it.trimEnd('/') + request.command.path } - val url = claimedUrl(token)?.takeIf { it in accepted } ?: accepted.firstOrNull() ?: return Proof.Refused(MachineReadablePrefix.AUTH_REQUIRED.format("this relay names no url to sign")) - return when (val r = verifier.verify(token, "POST", url, request.body)) { + if (accepted.isEmpty()) return Proof.Refused(MachineReadablePrefix.AUTH_REQUIRED.format("this relay names no url to sign")) + return when (val r = verifier.verify(token, "POST", accepted, request.body)) { is Nip98AuthVerifier.Result.Verified -> { Proof.Signed(r.pubkey) } @@ -325,15 +321,6 @@ class HttpRelayHandler( } } - /** The `u` a NIP-98 token names, read as the verifier reads it, or null when it does not decode. */ - @OptIn(ExperimentalEncodingApi::class) - private fun claimedUrl(token: String): String? = - runCatching { - val json = Base64.decode(token.removePrefix(Nip98AuthVerifier.SCHEME).trim()).decodeToString() - val event = OptimizedJsonMapper.fromJson(json) - HTTPAuthorizationEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig).url() - }.getOrNull() - private fun closed(reason: String) = withoutSubId(ClosedMessage(HttpRelayCommand.SUB_ID, reason).toJson()) companion object { diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt index 461012547d..1c42b052eb 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt @@ -436,16 +436,34 @@ class HttpRelayHandlerTest { } @Test - fun theBodyShapeIsReadWithoutATree() { + fun aBodyIsSplicedIntoItsFrameAsSent() { assertEquals("""["REQ","http",{"kinds":[1]}]""", HttpRelayCommand.REQ.frameOf(""" {"kinds":[1]} """)) assertEquals("""["COUNT","http",{"a":"]"},{"b":"\"["}]""", HttpRelayCommand.COUNT.frameOf("""[{"a":"]"},{"b":"\"["}]""")) assertEquals("""["EVENT",{"id":"x"}]""", HttpRelayCommand.EVENT.frameOf("""{"id":"x"}""")) - for (bad in listOf("", "[]", "{", "[{}", "{}}", "{]", "[{}]x", "{} {}", "[{},]", "[,{}]", "[{} {}]", "[1]", """[{},"x"]""", "null", "\"x\"")) { - assertEquals(null, HttpRelayCommand.REQ.frameOf(bad), "REQ '$bad'") - } + for (bad in listOf("", "[]", "[ ]", "[{}", "1", "null", "\"x\"")) assertEquals(null, HttpRelayCommand.REQ.frameOf(bad), "REQ '$bad'") for (bad in listOf("""[{"id":"x"}]""", "1")) assertEquals(null, HttpRelayCommand.EVENT.frameOf(bad), "EVENT '$bad'") } + @Test + fun aBodyCannotCarryASecondCommand() { + val smuggled = note("smuggled") + val answer = handler().ask(HttpRelayCommand.REQ, """{"kinds":[1]}],["EVENT",${smuggled.toJson()}""") + assertTrue(answer.lines.last().let { it == """["EOSE"]""" || it.startsWith("""["NOTICE",""") }, answer.lines.toString()) + assertTrue(backend.events.none { it.id == smuggled.id }, "only the REQ ran") + } + + @Test + fun aTokenSignedAtAnyOfTheRelaysAddressesVerifies() { + val onion = "http://relayxyz.onion" + val h = HttpRelayHandler(MemoryRelay(backend, true), origins = { listOf(origin, onion) }) + val body = """{"kinds":[1]}""" + + fun at(base: String) = alice.sign(HTTPAuthorizationEvent.build(base + "/req", "POST", body.encodeToByteArray(), System.currentTimeMillis() / 1000) {}).toAuthToken() + assertEquals(200, h.ask(HttpRelayCommand.REQ, body, at(onion)).status) + assertEquals(200, h.ask(HttpRelayCommand.REQ, body, at(origin)).status) + assertEquals(401, h.ask(HttpRelayCommand.REQ, body, at("https://elsewhere.example")).status) + } + private companion object { const val STALLED_KIND = 7 const val TRICKLE_KIND = 8 From 8187028c354634439454c0416cd52b5c5069471d Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sun, 27 Sep 2026 14:18:07 +0000 Subject: [PATCH 55/60] fix: audit findings in the code moved to commons Pre-existing bugs found while auditing the moved files, none introduced by the move: - showAmount/showAmountInteger rounded half-even (Kotlin's BigDecimal div) so 12,500 read "12k", and picked the unit before rounding so 999,500 read "1000k". - AgentWorkBoard.merge ordered every band by upvotes, not just the queue. - SuspendableConfirmation could resume twice (tap + dismiss) and left a dead dialog when the upload was cancelled. - TorDialogViewModel.save rejected Internal/Off over a hidden, invalid port, and accepted out-of-range ports for External. - ExpandingCirclesAnimation ran an infinite transition whenever the composer was open; the circles now exist only while recording and animate in the draw phase. - DebouncedPublisher.flush cancelled and restarted a publish already in flight (a second signer request). - RelayFeedViewModel read the wrong note for the DM list's first emission, and compared the flow instead of its value. - ScheduledFlag built a DateFormat per composition; Cashu discovery progress could step backwards; Desktop's chess view model kept polling after an account switch replaced it. New tests: ZapFormatterTest, SuspendableConfirmationTest, TorDialogViewModelTest, and a DebouncedPublisher case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkG4KTqmnprUK9BAcwAyc8 --- .../2026-08-30-commons-migration-sweep.md | 65 +++++++ .../amethyst/commons/model/buzz/AgentWork.kt | 4 +- .../service/upload/SuspendableConfirmation.kt | 25 ++- .../commons/tor/TorDialogViewModel.kt | 27 ++- .../commons/util/DebouncedPublisher.kt | 11 +- .../commons/viewmodels/RelayFeedViewModel.kt | 6 +- .../commons/cashu/CashuWalletDiscovery.kt | 8 +- .../amethyst/commons/util/ZapFormatter.kt | 32 ++-- .../commons/util/ZapFormatterNoDecimals.kt | 19 +- .../upload/SuspendableConfirmationTest.kt | 95 ++++++++++ .../commons/tor/TorDialogViewModelTest.kt | 61 +++++++ .../commons/util/DebouncedPublisherTest.kt | 26 +++ .../amethyst/commons/util/ZapFormatterTest.kt | 66 +++++++ .../commons/audio/RecordingIndicators.kt | 172 ++++++------------ .../ui/StreamingStatusFlags.kt | 21 ++- .../amethyst/desktop/chess/ChessScreen.kt | 6 + 16 files changed, 483 insertions(+), 161 deletions(-) create mode 100644 commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/service/upload/SuspendableConfirmationTest.kt create mode 100644 commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/tor/TorDialogViewModelTest.kt create mode 100644 commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/util/ZapFormatterTest.kt diff --git a/commons/plans/2026-08-30-commons-migration-sweep.md b/commons/plans/2026-08-30-commons-migration-sweep.md index c67ba3f177..e5ee522cd6 100644 --- a/commons/plans/2026-08-30-commons-migration-sweep.md +++ b/commons/plans/2026-08-30-commons-migration-sweep.md @@ -1686,3 +1686,68 @@ The same sweep finds 46 more movable files outside `ui/`. The notable groups are (`InFlightInvoices`, `PaymentPromptLedger`, the `bud10` resolver trio), `service/playback` leaves (`HlsLivenessCache`, `SessionRegistry`, `WssDataStreamCollector`, the player control buttons), and `model/MediaAspectRatioCache`. + +### Audit of this round + +Three reviewers read the 69 moved files. I read the chess merge and the hand edits. The +edits kept behaviour: `codePointAtKmp`/`codePointCharCount` match the JDK calls, +`padStart` matches `%02d` for every Int, `toInt()` throws the same exception as +`parseInt`, and lifecycle's `ViewModelProvider` calls the `KClass` factory overload +first. None of the findings below came from the move; all are on `main`. + +**Fixed:** + +- **Zap amounts rounded half-even** (`showAmount`, `showAmountInteger`). Kotlin's + `BigDecimal.div` already rounds to the dividend's scale with HALF_EVEN, so the + `setScale(0, HALF_UP)` after it never ran: 12,500 read "12k" and 2,500,000 read "2M". + The unit was also picked before rounding, so 999,500 read "1000k". Both now use + `divide(_, 0, HALF_UP)` and pick the unit after rounding. The `BigDecimal(0.01)` built + on every call is now a constant. The `showAmount` KDoc examples were wrong and now match + what it returns. `ZapFormatterTest` covers this. +- **`AgentWorkBoard.merge` sorted every band by upvotes.** Its KDoc says upvotes order + only the queue. An old upvoted job sat above a fresh one in Shipped, and every job sat + above every workflow run in Working. Upvotes now count only in the queue. +- **`SuspendableConfirmation`** could resume its continuation twice: a button tap plus + an outside-tap dismiss before the dialog recomposed away threw "Already resumed". A + cancelled upload also left the dialog up with dead buttons. It now resumes only while + active, and clears the dialog on cancellation, but only if the dialog is still its own. + `SuspendableConfirmationTest` covers this. +- **`TorDialogViewModel.save`** parsed the SOCKS port even with the field hidden. Junk + typed while External was selected blocked saving Internal or Off. It also took 0 or + 70000. Now only External requires a port in 1..65535; otherwise the last saved port is + kept. `TorDialogViewModelTest` covers this. +- **`ExpandingCirclesAnimation`** built its infinite transition before checking + `isRecording`. The composer toolbar always shows the record button, so it asked for a + frame on every vsync while the composer was open. The transition now exists only while + recording. The circles and the stop dot read their animated values in `graphicsLayer`, + so a frame redraws a layer instead of recomposing. +- **`DebouncedPublisher.flush`** treated a job whose `publish()` was already running as + pending. Leaving the nav picker while a publish waited on a remote signer cancelled it + and started a second signing request. `flush` now acts only during the delay. A new case + in `DebouncedPublisherTest` covers this. +- **`RelayFeedViewModel`:** + - The DM list's first emission read `nip65RelayListNote` as a `DmRelayListEvent`, + which is always null. It now reads `dmRelayListNote`. + - `subscribeTo`/`unsubscribeTo` compared the `MutableStateFlow` itself to a `User`, + so `unsubscribeTo` never cleared it. They now compare `.value`. +- **`ScheduledFlag`** built a `DateFormat` on every composition of a feed card. It is + now remembered per start time. +- **`CashuWalletDiscovery`:** up to 50 relays finishing at once could write the crawl + progress out of order, so it stepped backwards. The write is now monotonic. +- **Desktop `ChessScreen`:** polling runs on the screen's scope. When `remember` replaced + the view model on an account switch, the old one kept polling. A `DisposableEffect` now + stops it. + +**Reported, not changed (need an owner's call):** + +- `FilterLastMessageFollowingPublicChats` asks each relay for kind 41 with `limit = 1` + across all followed channels, so only one channel's metadata comes back per relay. One + busy channel can also fill the kind-42 `limit = 100`. The fix (a limit per channel, or a + filter per channel) changes relay load. +- `PitchShifter`'s brute-force WSOLA search costs about 500 multiply-adds per output + sample, and the loop never checks for cancellation. Decimating the correlation changes + the audio, and a cancellation check changes its API. +- `RelayFeedViewModel.invalidateData` re-emits the same user into a `StateFlow`, which + drops it, so a refresh only spins for a second. +- `ScheduledFlag` still reads `TimeUtils.now()` once, so a card composed before the start + time keeps the date after it passes. diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/buzz/AgentWork.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/buzz/AgentWork.kt index 98d2de1c56..15897a6b29 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/buzz/AgentWork.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/buzz/AgentWork.kt @@ -132,7 +132,9 @@ object AgentWorkBoard { (runs.map { from(it) } + jobs.map { from(it) }) .sortedWith( compareBy<AgentWorkItem> { it.state.ordinal } - .thenByDescending { it.upvotes ?: 0 } + // Upvotes are the queue's priority signal only. Elsewhere they would put an old + // upvoted job above a fresh one, and every job above every workflow run. + .thenByDescending { if (it.state == AgentWorkState.QUEUED) it.upvotes ?: 0 else 0 } .thenByDescending { it.updatedAt }, ) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/SuspendableConfirmation.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/SuspendableConfirmation.kt index 44a457a83c..3b4c1bac21 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/SuspendableConfirmation.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/SuspendableConfirmation.kt @@ -44,17 +44,24 @@ class SuspendableConfirmation { suspend fun awaitConfirmation(): Boolean = mutex.withLock { suspendCancellableCoroutine { continuation -> - state = + var callbacks: ConfirmationCallbacks? = null + + // The dialog can fire two of its callbacks (a button tap and an outside-tap + // dismiss) before the recomposition that removes it; only the first may resume. + fun answer(confirmed: Boolean) { + if (state === callbacks) state = null + if (continuation.isActive) continuation.resume(confirmed) + } + + callbacks = ConfirmationCallbacks( - onConfirm = { - state = null - continuation.resume(true) - }, - onCancel = { - state = null - continuation.resume(false) - }, + onConfirm = { answer(true) }, + onCancel = { answer(false) }, ) + state = callbacks + + // An upload cancelled while waiting must not leave a dialog with dead buttons. + continuation.invokeOnCancellation { if (state === callbacks) state = null } } } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/tor/TorDialogViewModel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/tor/TorDialogViewModel.kt index 8158665f84..a01c9a2054 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/tor/TorDialogViewModel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/tor/TorDialogViewModel.kt @@ -65,8 +65,12 @@ class TorDialogViewModel : ViewModel() { ) } + /** The last port that was actually saved, kept when the field is hidden and holds junk. */ + private var savedSocksPort = TorSettings().externalSocksPort + fun reset(torSettings: TorSettings) { torType.value = torSettings.torType + savedSocksPort = torSettings.externalSocksPort socksPortStr.value = torSettings.externalSocksPort.toString() onionRelaysViaTor.value = torSettings.onionRelaysViaTor dmRelaysViaTor.value = torSettings.dmRelaysViaTor @@ -81,10 +85,26 @@ class TorDialogViewModel : ViewModel() { mediaUploadsViaTor.value = torSettings.mediaUploadsViaTor } - fun save(): TorSettings = - TorSettings( + /** + * The port field is only shown for [TorType.EXTERNAL], so only then is a bad value the user's + * to fix: it throws, and the dialog shows its "invalid port" message. With the field hidden, a + * leftover unparsable value must not block saving Internal or Off; the last saved port stays. + */ + fun save(): TorSettings { + val typedPort = + socksPortStr.value + .trim() + .toIntOrNull() + ?.takeIf { it in 1..65535 } + val port = + when { + typedPort != null -> typedPort + torType.value == TorType.EXTERNAL -> throw IllegalArgumentException("Invalid SOCKS port: ${socksPortStr.value}") + else -> savedSocksPort + } + return TorSettings( torType = torType.value, - externalSocksPort = socksPortStr.value.toInt(), + externalSocksPort = port, onionRelaysViaTor = onionRelaysViaTor.value, dmRelaysViaTor = dmRelaysViaTor.value, newRelaysViaTor = newRelaysViaTor.value, @@ -97,6 +117,7 @@ class TorDialogViewModel : ViewModel() { nip05VerificationsViaTor = nip05VerificationsViaTor.value, mediaUploadsViaTor = mediaUploadsViaTor.value, ) + } fun setPreset(preset: TorPresetType) { when (preset) { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/DebouncedPublisher.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/DebouncedPublisher.kt index eff052fcb0..c361099f9c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/DebouncedPublisher.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/util/DebouncedPublisher.kt @@ -45,6 +45,13 @@ class DebouncedPublisher( ) { private var pending: Job? = null + /** + * True only while [pending] is still waiting out its delay. Once [publish] has started, the + * job is still active but there is nothing left to hurry: flushing then would cancel a publish + * that may be parked on a remote signer and start a second one. + */ + private var waiting = false + /** Records an edit: restarts the wait, so a run of edits publishes once, after the last one. */ fun schedule() = start(debounceMs) @@ -53,15 +60,17 @@ class DebouncedPublisher( * pending, so a caller can flush on every exit path without publishing the same state twice. */ fun flush() { - if (pending?.isActive != true) return + if (!waiting || pending?.isActive != true) return start(0) } private fun start(delayMs: Long) { pending?.cancel() + waiting = true pending = launch { delay(delayMs) + waiting = false publish() } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/RelayFeedViewModel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/RelayFeedViewModel.kt index be771d2bf8..37845cb984 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/RelayFeedViewModel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/RelayFeedViewModel.kt @@ -155,7 +155,7 @@ class RelayFeedViewModel : emit(emptyList()) } }.onStart { - emit(convert((currentUser.value?.nip65RelayListNote?.event as? DmRelayListEvent)?.relays()?.toSet(), currentUser.value?.relayState()?.data)) + emit(convert((currentUser.value?.dmRelayListNote?.event as? DmRelayListEvent)?.relays()?.toSet(), currentUser.value?.relayState()?.data)) }.flowOn(Dispatchers.IO) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) @@ -179,13 +179,13 @@ class RelayFeedViewModel : @OptIn(FlowPreview::class) fun subscribeTo(user: User) { - if (currentUser != user) { + if (currentUser.value != user) { currentUser.tryEmit(user) } } fun unsubscribeTo(user: User) { - if (currentUser == user) { + if (currentUser.value == user) { currentUser.tryEmit(null) invalidateData() } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/CashuWalletDiscovery.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/CashuWalletDiscovery.kt index 54aae743e5..772e18e7fb 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/CashuWalletDiscovery.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/CashuWalletDiscovery.kt @@ -36,6 +36,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope @@ -157,7 +158,12 @@ class CashuWalletDiscovery( } }, onRelayComplete = { - _state.value = State.Crawling(completed.incrementAndGet(), relays.size) + // Up to MAX_CONCURRENT_RELAYS relays finish at once: a coroutine preempted + // between the increment and the write would move the progress backwards. + val done = completed.incrementAndGet() + _state.update { current -> + if (current is State.Crawling && current.relaysCompleted >= done) current else State.Crawling(done, relays.size) + } }, ) } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/ZapFormatter.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/ZapFormatter.kt index a0ad25666c..1e32ae89c6 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/ZapFormatter.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/ZapFormatter.kt @@ -31,6 +31,9 @@ val OneMega = BigDecimal(1_000_000) val TenKilo = BigDecimal(10_000) val OneKilo = BigDecimal(1_000) +/** Below this an amount rounds to nothing. Built once: it was a new `BigDecimal` per call. */ +internal val MinDisplayableAmount = BigDecimal(0.01) + private val dfGBig = ThreadLocal.withInitial { DecimalFormat("#.#G") } private val dfGSmall = ThreadLocal.withInitial { DecimalFormat("#.0G") } private val dfMBig = ThreadLocal.withInitial { DecimalFormat("#.#M") } @@ -42,23 +45,28 @@ private val dfN = ThreadLocal.withInitial { DecimalFormat("#") } * Formats a BigDecimal amount to human-readable format with G/M/K suffixes. * Returns empty string for null or very small amounts. * + * Amounts are rounded half-up to a whole number of the unit before the unit is picked, so + * 999,500 reads "1.0M" rather than "1000k". + * * Examples: - * - 1500 -> "1.5k" - * - 2500000 -> "2.5M" + * - 1500 -> "1500" + * - 12500 -> "13k" + * - 2500000 -> "3.0M" * - 10000000000 -> "10G" */ fun showAmount(amount: BigDecimal?): String { if (amount == null) return "" - if (amount.abs() < BigDecimal(0.01)) return "" + if (amount.abs() < MinDisplayableAmount) return "" + if (amount < TenKilo) return dfN.get()!!.format(amount) - return when { - amount >= TenGiga -> dfGBig.get()!!.format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP)) - amount >= OneGiga -> dfGSmall.get()!!.format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP)) - amount >= TenMega -> dfMBig.get()!!.format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP)) - amount >= OneMega -> dfMSmall.get()!!.format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP)) - amount >= TenKilo -> dfK.get()!!.format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP)) - else -> dfN.get()!!.format(amount) - } + // `divide(_, 0, HALF_UP)`, not `div(_).setScale(0, HALF_UP)`: Kotlin's `div` already rounds + // to the dividend's scale with HALF_EVEN, so the HALF_UP never ran and 12,500 read "12k". + val kilos = amount.divide(OneKilo, 0, RoundingMode.HALF_UP) + if (kilos < OneKilo) return dfK.get()!!.format(kilos) + val megas = amount.divide(OneMega, 0, RoundingMode.HALF_UP) + if (megas < OneKilo) return (if (megas < BigDecimal.TEN) dfMSmall else dfMBig).get()!!.format(megas) + val gigas = amount.divide(OneGiga, 0, RoundingMode.HALF_UP) + return (if (gigas < BigDecimal.TEN) dfGSmall else dfGBig).get()!!.format(gigas) } /** @@ -67,7 +75,7 @@ fun showAmount(amount: BigDecimal?): String { */ fun showAmountWithZero(amount: BigDecimal?): String { if (amount == null) return "0" - if (amount.abs() < BigDecimal(0.01)) return "0" + if (amount.abs() < MinDisplayableAmount) return "0" return showAmount(amount) } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/ZapFormatterNoDecimals.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/ZapFormatterNoDecimals.kt index 5207240aef..8dc0ae07d1 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/ZapFormatterNoDecimals.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/ZapFormatterNoDecimals.kt @@ -46,14 +46,17 @@ private val dfN = fun showAmountInteger(amount: BigDecimal?): String { if (amount == null) return "" - if (amount.abs() < BigDecimal(0.01)) return "" + if (amount.abs() < MinDisplayableAmount) return "" + if (amount < TenKilo) return dfN.get()?.format(amount) ?: "" - return when { - amount >= OneGiga -> dfG.get()?.format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP)) ?: "" - amount >= OneMega -> dfM.get()?.format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP)) ?: "" - amount >= TenKilo -> dfK.get()?.format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP)) ?: "" - else -> dfN.get()?.format(amount) ?: "" - } + // Round before picking the unit, so 999,500 reads "1M" rather than "1000k". + // `divide(_, 0, HALF_UP)` rather than `div(_).setScale(0, HALF_UP)`: Kotlin's `div` already + // rounds to the dividend's scale with HALF_EVEN, which left 12,500 as "12k". + val kilos = amount.divide(OneKilo, 0, RoundingMode.HALF_UP) + if (kilos < OneKilo) return dfK.get()?.format(kilos) ?: "" + val megas = amount.divide(OneMega, 0, RoundingMode.HALF_UP) + if (megas < OneKilo) return dfM.get()?.format(megas) ?: "" + return dfG.get()?.format(amount.divide(OneGiga, 0, RoundingMode.HALF_UP)) ?: "" } fun showAmountInteger(amount: Int?): String { @@ -64,7 +67,7 @@ fun showAmountInteger(amount: Int?): String { fun showAmountIntegerWithZero(amount: BigDecimal?): String { if (amount == null) return "0" - if (amount.abs() < BigDecimal(0.01)) return "0" + if (amount.abs() < MinDisplayableAmount) return "0" return showAmountInteger(amount) } diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/service/upload/SuspendableConfirmationTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/service/upload/SuspendableConfirmationTest.kt new file mode 100644 index 0000000000..9befbfb67e --- /dev/null +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/service/upload/SuspendableConfirmationTest.kt @@ -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.commons.service.upload + +import kotlinx.coroutines.async +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test + +class SuspendableConfirmationTest { + @Test + fun confirmResumesTheWaiterAndClosesTheDialog() = + runTest { + val confirmation = SuspendableConfirmation() + val answer = async { confirmation.awaitConfirmation() } + runCurrent() + + assertNotNull(confirmation.state) + confirmation.state!!.onConfirm() + + assertEquals(true, answer.await()) + assertNull(confirmation.state) + } + + @Test + fun twoCallbacksFromTheSameDialogResumeOnlyOnce() = + runTest { + // A button tap and an outside-tap dismiss can both land before the dialog recomposes + // away. The second used to resume the continuation again and throw. + val confirmation = SuspendableConfirmation() + val answer = async { confirmation.awaitConfirmation() } + runCurrent() + + val callbacks = confirmation.state!! + callbacks.onConfirm() + callbacks.onCancel() + + assertEquals(true, answer.await()) + assertNull(confirmation.state) + } + + @Test + fun cancellingTheWaiterClosesTheDialog() = + runTest { + val confirmation = SuspendableConfirmation() + val answer = async { confirmation.awaitConfirmation() } + runCurrent() + assertNotNull(confirmation.state) + + answer.cancel() + runCurrent() + + assertNull(confirmation.state) + } + + @Test + fun aStaleCallbackDoesNotCloseTheNextDialog() = + runTest { + val confirmation = SuspendableConfirmation() + val first = async { confirmation.awaitConfirmation() } + runCurrent() + val firstCallbacks = confirmation.state!! + firstCallbacks.onCancel() + assertEquals(false, first.await()) + + val second = async { confirmation.awaitConfirmation() } + runCurrent() + firstCallbacks.onConfirm() + + assertNotNull(confirmation.state) + confirmation.state!!.onConfirm() + assertEquals(true, second.await()) + } +} diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/tor/TorDialogViewModelTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/tor/TorDialogViewModelTest.kt new file mode 100644 index 0000000000..01092252ff --- /dev/null +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/tor/TorDialogViewModelTest.kt @@ -0,0 +1,61 @@ +/* + * 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.commons.tor + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Test + +class TorDialogViewModelTest { + private fun dialogFor(settings: TorSettings) = TorDialogViewModel().apply { reset(settings) } + + @Test + fun externalSavesTheTypedPort() { + val dialog = dialogFor(TorSettings(torType = TorType.EXTERNAL, externalSocksPort = 9050)) + dialog.socksPortStr.value = "9150" + + assertEquals(9150, dialog.save().externalSocksPort) + } + + @Test + fun externalRejectsAPortThatIsNotANumberOrOutOfRange() { + val dialog = dialogFor(TorSettings(torType = TorType.EXTERNAL)) + + for (bad in listOf("90a0", "", "0", "70000", "-1")) { + dialog.socksPortStr.value = bad + assertThrows(IllegalArgumentException::class.java) { dialog.save() } + } + } + + @Test + fun aHiddenJunkPortDoesNotBlockSavingInternal() { + // The port field only shows for External. Typing junk there and then switching to + // Internal used to make every save fail with "invalid port", with the field hidden. + val dialog = dialogFor(TorSettings(torType = TorType.EXTERNAL, externalSocksPort = 9150)) + dialog.socksPortStr.value = "90a0" + dialog.torType.value = TorType.INTERNAL + + val saved = dialog.save() + + assertEquals(TorType.INTERNAL, saved.torType) + assertEquals(9150, saved.externalSocksPort) + } +} diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/util/DebouncedPublisherTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/util/DebouncedPublisherTest.kt index d90d6a823a..40636cf461 100644 --- a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/util/DebouncedPublisherTest.kt +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/util/DebouncedPublisherTest.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy @@ -103,6 +104,31 @@ class DebouncedPublisherTest { assertEquals(1, published) } + @Test + fun flushDuringAnInFlightPublishDoesNotRestartIt() = + runTest { + // A publish can sit for seconds waiting on a remote signer. Leaving the screen then + // flushes; that must not cancel the signing request and send a second one. + var started = 0 + var finished = 0 + val publisher = + publisher { + started++ + delay(5_000) + finished++ + } + + publisher.schedule() + advanceTimeBy(debounce + 100) + assertEquals(1, started) + + publisher.flush() + advanceUntilIdle() + + assertEquals(1, started) + assertEquals(1, finished) + } + @Test fun flushWithNothingPendingIsANoOp() = runTest { diff --git a/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/util/ZapFormatterTest.kt b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/util/ZapFormatterTest.kt new file mode 100644 index 0000000000..634aee9390 --- /dev/null +++ b/commons/src/jvmTest/kotlin/com/vitorpamplona/amethyst/commons/util/ZapFormatterTest.kt @@ -0,0 +1,66 @@ +/* + * 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.commons.util + +import org.junit.Assert.assertEquals +import org.junit.Test +import java.math.BigDecimal +import java.util.Locale + +class ZapFormatterTest { + private fun <T> inEnglish(block: () -> T): T { + val previous = Locale.getDefault() + Locale.setDefault(Locale.US) + try { + return block() + } finally { + Locale.setDefault(previous) + } + } + + @Test + fun halvesRoundUpNotToEven() = + inEnglish { + // Kotlin's BigDecimal `div` rounds HALF_EVEN, so 12,500 used to read "12k". + assertEquals("13k", showAmountInteger(12_500)) + assertEquals("14k", showAmountInteger(13_500)) + assertEquals("3M", showAmountInteger(2_500_000)) + assertEquals("13k", showAmount(BigDecimal(12_500))) + } + + @Test + fun roundingUpToTheNextUnitSwitchesUnit() = + inEnglish { + assertEquals("999k", showAmountInteger(999_499)) + assertEquals("1M", showAmountInteger(999_500)) + assertEquals("1G", showAmountInteger(999_500_000)) + assertEquals("1.0M", showAmount(BigDecimal(999_500))) + } + + @Test + fun smallAmountsStayWhole() = + inEnglish { + assertEquals("0", showAmountInteger(0)) + assertEquals("9999", showAmountInteger(9_999)) + assertEquals("", showAmount(BigDecimal("0.001"))) + assertEquals("0", showAmountWithZero(BigDecimal("0.001"))) + } +} diff --git a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/audio/RecordingIndicators.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/audio/RecordingIndicators.kt index be2eb5eced..63b8a299b6 100644 --- a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/audio/RecordingIndicators.kt +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/audio/RecordingIndicators.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.audio +import androidx.compose.animation.core.InfiniteTransition import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat @@ -29,6 +30,7 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -38,12 +40,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -63,113 +63,56 @@ fun ExpandingCirclesAnimation( modifier: Modifier = Modifier, isRecording: Boolean, primaryColor: Color = MaterialTheme.colorScheme.primary, +) { + // The composer shows this button all the time. An infinite transition asks for a frame on + // every vsync for as long as it is composed, so it only exists while recording. + if (isRecording) { + ExpandingCircles(modifier, primaryColor) + } +} + +@Composable +private fun ExpandingCircles( + modifier: Modifier, + primaryColor: Color, ) { val infiniteTransition = rememberInfiniteTransition(label = "expanding_circles") - // First circle animation - val scale1 by infiniteTransition.animateFloat( - initialValue = 0f, - targetValue = 2.5f, - animationSpec = - infiniteRepeatable( - animation = tween(durationMillis = 1500, easing = LinearEasing), - repeatMode = RepeatMode.Restart, - ), - label = "circle1_scale", - ) - - val alpha1 by infiniteTransition.animateFloat( - initialValue = 1f, - targetValue = 0f, - animationSpec = - infiniteRepeatable( - animation = tween(durationMillis = 1500, easing = LinearEasing), - repeatMode = RepeatMode.Restart, - ), - label = "circle1_alpha", - ) - - // Second circle animation (offset by 500ms) - val scale2 by infiniteTransition.animateFloat( - initialValue = 0f, - targetValue = 2.5f, - animationSpec = - infiniteRepeatable( - animation = tween(durationMillis = 1500, delayMillis = 500, easing = LinearEasing), - repeatMode = RepeatMode.Restart, - ), - label = "circle2_scale", - ) - - val alpha2 by infiniteTransition.animateFloat( - initialValue = 1f, - targetValue = 0f, - animationSpec = - infiniteRepeatable( - animation = tween(durationMillis = 1500, delayMillis = 500, easing = LinearEasing), - repeatMode = RepeatMode.Restart, - ), - label = "circle2_alpha", - ) - - // Third circle animation (offset by 1000ms) - val scale3 by infiniteTransition.animateFloat( - initialValue = 0f, - targetValue = 2.5f, - animationSpec = - infiniteRepeatable( - animation = tween(durationMillis = 1500, delayMillis = 1000, easing = LinearEasing), - repeatMode = RepeatMode.Restart, - ), - label = "circle3_scale", - ) - - val alpha3 by infiniteTransition.animateFloat( - initialValue = 1f, - targetValue = 0f, - animationSpec = - infiniteRepeatable( - animation = tween(durationMillis = 1500, delayMillis = 1000, easing = LinearEasing), - repeatMode = RepeatMode.Restart, - ), - label = "circle3_alpha", - ) - - if (!isRecording) return - Box(modifier = modifier, contentAlignment = Alignment.Center) { - // Circle 1 - Box( - modifier = - Modifier - .matchParentSize() - .scale(scale1) - .alpha(alpha1) - .background(primaryColor.copy(alpha = 0.3f), CircleShape), - ) - - // Circle 2 - Box( - modifier = - Modifier - .matchParentSize() - .scale(scale2) - .alpha(alpha2) - .background(primaryColor.copy(alpha = 0.2f), CircleShape), - ) - - // Circle 3 - Box( - modifier = - Modifier - .matchParentSize() - .scale(scale3) - .alpha(alpha3) - .background(primaryColor.copy(alpha = 0.1f), CircleShape), - ) + ExpandingCircle(infiniteTransition, delayMillis = 0, color = primaryColor.copy(alpha = 0.3f), label = "circle1") + ExpandingCircle(infiniteTransition, delayMillis = 500, color = primaryColor.copy(alpha = 0.2f), label = "circle2") + ExpandingCircle(infiniteTransition, delayMillis = 1000, color = primaryColor.copy(alpha = 0.1f), label = "circle3") } } +@Composable +private fun BoxScope.ExpandingCircle( + transition: InfiniteTransition, + delayMillis: Int, + color: Color, + label: String, +) { + val spec = + infiniteRepeatable<Float>( + animation = tween(durationMillis = 1500, delayMillis = delayMillis, easing = LinearEasing), + repeatMode = RepeatMode.Restart, + ) + val scale = transition.animateFloat(initialValue = 0f, targetValue = 2.5f, animationSpec = spec, label = "${label}_scale") + val alpha = transition.animateFloat(initialValue = 1f, targetValue = 0f, animationSpec = spec, label = "${label}_alpha") + + // Read in the draw phase so each frame redraws the layer instead of recomposing. + Box( + modifier = + Modifier + .matchParentSize() + .graphicsLayer { + scaleX = scale.value + scaleY = scale.value + this.alpha = alpha.value + }.background(color, CircleShape), + ) +} + /** * Floating recording indicator showing elapsed time */ @@ -218,16 +161,17 @@ fun FloatingRecordingIndicator( ) { // Pulsing stop square val infiniteTransition = rememberInfiniteTransition(label = "recording_stop") - val dotAlpha by infiniteTransition.animateFloat( - initialValue = 1f, - targetValue = 0.5f, - animationSpec = - infiniteRepeatable( - animation = tween(durationMillis = 1000), - repeatMode = RepeatMode.Reverse, - ), - label = "dot_alpha", - ) + val dotAlpha = + infiniteTransition.animateFloat( + initialValue = 1f, + targetValue = 0.5f, + animationSpec = + infiniteRepeatable( + animation = tween(durationMillis = 1000), + repeatMode = RepeatMode.Reverse, + ), + label = "dot_alpha", + ) Icon( symbol = MaterialSymbols.Stop, @@ -235,7 +179,7 @@ fun FloatingRecordingIndicator( tint = Color.White, modifier = Modifier - .alpha(dotAlpha) + .graphicsLayer { alpha = dotAlpha.value } .padding(end = 8.dp), ) diff --git a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip53LiveActivities/ui/StreamingStatusFlags.kt b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip53LiveActivities/ui/StreamingStatusFlags.kt index 7528de89ad..cfc643fb71 100644 --- a/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip53LiveActivities/ui/StreamingStatusFlags.kt +++ b/commonsUI/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/nip53LiveActivities/ui/StreamingStatusFlags.kt @@ -112,16 +112,19 @@ fun OfflineFlag() { @Composable fun ScheduledFlag(starts: Long?) { + // Remembered: this sits on feed cards, and building a DateFormat loads locale data each time. val startsIn = - starts?.let { - if (it > TimeUtils.now()) { - SimpleDateFormat - .getDateTimeInstance( - DateFormat.SHORT, - DateFormat.SHORT, - ).format(Date(starts * 1000)) - } else { - null + remember(starts) { + starts?.let { + if (it > TimeUtils.now()) { + SimpleDateFormat + .getDateTimeInstance( + DateFormat.SHORT, + DateFormat.SHORT, + ).format(Date(starts * 1000)) + } else { + null + } } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt index 271312a224..4f35817288 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt @@ -48,6 +48,7 @@ import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -116,6 +117,11 @@ fun ChessScreen( scope = scope, ) } + // The polling runs on this screen's scope, which outlives an account switch: stop the old + // account's view model when `remember` replaces it, or both keep polling. + DisposableEffect(viewModel) { + onDispose { viewModel.stopPolling() } + } val connectedRelays by relayManager.connectedRelays.collectAsState() val broadcastStatus by viewModel.broadcastStatus.collectAsState() val activeGames by viewModel.activeGames.collectAsState() From 2442a12338acb5f7159bd38a89d5ab0c74cbf965 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sun, 27 Sep 2026 14:32:33 +0000 Subject: [PATCH 56/60] Leave Nip98AuthVerifier as on main NIP-86 keeps the verifier exactly as it was: no rejectReplays, no maxReplayEntries, no URL-list overload. The NIP-FE handler gets non-single-use tokens from a fresh verifier per request, trying each of the relay's addresses in turn, and loses its verifier parameter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jDNhr6a3J4VC5TaG3Yd48 --- .../quartz/nip98HttpAuth/Nip98AuthVerifier.kt | 56 ++++++------------- .../nipFERelayOverHttp/HttpRelayHandler.kt | 32 ++++------- .../nip98HttpAuth/Nip98AuthVerifierTest.kt | 36 ------------ .../HttpRelayHandlerTest.kt | 17 ------ 4 files changed, 28 insertions(+), 113 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifier.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifier.kt index 06d0ce31cd..ebd580ee15 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifier.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifier.kt @@ -42,10 +42,8 @@ import kotlin.math.abs * 2. Decoded body is a kind-27235 event with a valid Schnorr signature. * 3. The event's `created_at` is within ±[toleranceSeconds] of now. * 4. The `method` tag matches the HTTP method. - * 5. The `u` tag is the requested URL, or one of them: a server reachable at more than one - * address (a clearnet host and an .onion) accepts a token signed at any. + * 5. The `u` tag matches the requested URL. * 6. If a body is present, the `payload` tag matches `sha256(body)` hex. - * 7. With [rejectReplays], the token has not been accepted before. * * Returns the verified pubkey on success; a [Result.Malformed] / * [Result.Missing] otherwise (the caller turns these into 401/403). @@ -54,22 +52,10 @@ class Nip98AuthVerifier( private val now: () -> Long = { TimeUtils.now() }, /** Allowed clock skew in seconds. NIP-98 says 60. */ private val toleranceSeconds: Long = 60, - /** - * Tokens remembered at once. When every remembered token is still inside its window, a new one - * is refused (`rate-limited:`) rather than an unexpired one forgotten: forgetting is what lets a - * captured token be replayed. Size it for the endpoint's signed-request rate over 2 x tolerance. - */ - private val maxReplayEntries: Int = MAX_REPLAY_ENTRIES, - /** - * Accept each token once. NIP-98 does not ask for it, and the memory is this process's alone, so - * behind a load balancer it holds per instance. Off, a token captured in its window can repeat - * the request it signs, and only that request when it binds the body's hash. - */ - private val rejectReplays: Boolean = true, ) { /** * Recently-accepted event ids → expiry epoch second. Bounded to - * [maxReplayEntries], never by evicting a live entry; each entry expires + * [MAX_REPLAY_ENTRIES] (insertion-order eviction); each entry expires * after `2 × toleranceSeconds` (twice the accepted window so a token * can't be reused by an attacker who buffers across the boundary). * @@ -81,18 +67,11 @@ class Nip98AuthVerifier( private val seenLock = Mutex() - suspend fun verify( - authorizationHeader: String?, - method: String, - url: String, - body: ByteArray?, - ): Result = verify(authorizationHeader, method, listOf(url), body) - @OptIn(ExperimentalEncodingApi::class) suspend fun verify( authorizationHeader: String?, method: String, - urls: Collection<String>, + url: String, body: ByteArray?, ): Result { if (authorizationHeader.isNullOrBlank()) return Result.Missing @@ -138,8 +117,8 @@ class Nip98AuthVerifier( if (!auth.method().equals(method, ignoreCase = true)) { return Result.Malformed("method mismatch: expected $method, got ${auth.method()}") } - if (auth.url() !in urls) { - return Result.Malformed("url mismatch: expected ${urls.joinToString(" or ")}, got ${auth.url()}") + if (auth.url() != url) { + return Result.Malformed("url mismatch: expected $url, got ${auth.url()}") } if (body != null && body.isNotEmpty()) { val expected = sha256(body).toHexKey() @@ -148,8 +127,6 @@ class Nip98AuthVerifier( } } - if (!rejectReplays) return Result.Verified(event.pubKey) - // Replay check — done LAST so we don't burn a one-shot id on a // request that would otherwise have failed signature/url/etc. val expiry = nowSec + 2 * toleranceSeconds @@ -163,13 +140,18 @@ class Nip98AuthVerifier( while (it.hasNext()) { if (it.next().value <= nowSec) it.remove() else break } - if (seenEventIds.containsKey(event.id)) { + if (seenEventIds.put(event.id, expiry) != null) { return Result.Malformed("replay: this NIP-98 token has already been used") } - if (seenEventIds.size >= maxReplayEntries) { - return Result.Malformed(REPLAY_CACHE_FULL) + // Cap entries: drop oldest by insertion order. Equivalent to + // the JDK LinkedHashMap.removeEldestEntry hook we used before, + // but works in KMP commonMain. + while (seenEventIds.size > MAX_REPLAY_ENTRIES) { + val eldest = seenEventIds.keys.iterator() + if (!eldest.hasNext()) break + eldest.next() + eldest.remove() } - seenEventIds[event.id] = expiry } return Result.Verified(event.pubKey) @@ -191,13 +173,11 @@ class Nip98AuthVerifier( const val SCHEME = "Nostr " /** - * Default cap on the in-memory replay cache: with a 60s tolerance, 1024 live tokens is about - * 8 signed requests a second, generous for an admin endpoint. A public endpoint passes a - * larger `maxReplayEntries`. + * Cap on the in-memory replay-cache size. With a 60s tolerance + * an attacker would need to push >MAX/120 verified requests per + * second (one new id per ~120 ms) to evict legitimate entries. + * 1024 is generous for an admin endpoint. */ const val MAX_REPLAY_ENTRIES = 1024 - - /** Answered when the replay cache holds nothing but live tokens. */ - const val REPLAY_CACHE_FULL = "rate-limited: too many fresh NIP-98 tokens at once; retry shortly" } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt index 90b611720e..a4054bbd08 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandler.kt @@ -95,12 +95,6 @@ class HttpRelayHandler( private val origins: () -> List<String>, /** How long one answer may run, first byte to last. [Duration.INFINITE] turns the deadline off. */ private val deadline: Duration = DEFAULT_DEADLINE, - /** - * Tokens are not single-use: a request can land on any instance, which a per-process memory of - * spent tokens cannot follow, and the body's hash already limits a captured token to the one - * command it signs, inside its window. - */ - private val verifier: Nip98AuthVerifier = Nip98AuthVerifier(rejectReplays = false), /** Frames queued ahead of a slow reader before the answer is cut short. */ private val maxQueuedFrames: Int = DEFAULT_MAX_QUEUED_FRAMES, /** How long past the deadline the last line may take before the reader counts as stalled. */ @@ -301,24 +295,18 @@ class HttpRelayHandler( val token = scheme + header.substring(scheme.length).trim() val accepted = origins().map { it.trimEnd('/') + request.command.path } if (accepted.isEmpty()) return Proof.Refused(MachineReadablePrefix.AUTH_REQUIRED.format("this relay names no url to sign")) - return when (val r = verifier.verify(token, "POST", accepted, request.body)) { - is Nip98AuthVerifier.Result.Verified -> { - Proof.Signed(r.pubkey) - } - - is Nip98AuthVerifier.Result.Missing -> { - Proof.Anonymous - } - - // A full replay cache, in a verifier that keeps one, is the relay's limit, not the token's fault. - is Nip98AuthVerifier.Result.Malformed -> { - if (MachineReadablePrefix.parse(r.reason) == MachineReadablePrefix.RATE_LIMITED) { - Proof.Refused(r.reason) - } else { - Proof.Refused(MachineReadablePrefix.AUTH_REQUIRED.format("NIP-98 ${r.reason}")) - } + // A fresh verifier each time: it remembers the tokens it accepts, and a NIP-FE token is not + // single-use. A request can land on any instance, which no one process's memory can follow, + // and the body's hash already limits a captured token to the command it signs, in its window. + var refusal: Nip98AuthVerifier.Result.Malformed? = null + for (url in accepted) { + when (val r = Nip98AuthVerifier().verify(token, "POST", url, request.body)) { + is Nip98AuthVerifier.Result.Verified -> return Proof.Signed(r.pubkey) + is Nip98AuthVerifier.Result.Missing -> return Proof.Anonymous + is Nip98AuthVerifier.Result.Malformed -> refusal = refusal ?: r } } + return Proof.Refused(MachineReadablePrefix.AUTH_REQUIRED.format("NIP-98 ${refusal?.reason}")) } private fun closed(reason: String) = withoutSubId(ClosedMessage(HttpRelayCommand.SUB_ID, reason).toJson()) diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifierTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifierTest.kt index d92e411946..2323410d5b 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifierTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifierTest.kt @@ -131,40 +131,4 @@ class Nip98AuthVerifierTest { assertTrue(r.reason.contains("kind")) } } - - @Test - fun aTokenIsAcceptedOnce() { - runBlocking { - val body = "hello".encodeToByteArray() - val (_, header) = signedToken("http://x/", "POST", body) - assertIs<Nip98AuthVerifier.Result.Verified>(verifier.verify(header, "POST", "http://x/", body)) - val again = verifier.verify(header, "POST", "http://x/", body) - assertIs<Nip98AuthVerifier.Result.Malformed>(again) - assertTrue(again.reason.contains("replay")) - } - } - - @Test - fun aFullCacheRefusesTheNewTokenAndStillRemembersTheOld() { - runBlocking { - val small = Nip98AuthVerifier(now = { 1_000L }, maxReplayEntries = 2) - val tokens = (0..2).map { n -> "body $n".encodeToByteArray().let { it to signedToken("http://x/", "POST", it).second } } - for ((body, header) in tokens.take(2)) assertIs<Nip98AuthVerifier.Result.Verified>(small.verify(header, "POST", "http://x/", body)) - val (body, header) = tokens[2] - assertEquals(Nip98AuthVerifier.Result.Malformed(Nip98AuthVerifier.REPLAY_CACHE_FULL), small.verify(header, "POST", "http://x/", body)) - val (oldBody, oldHeader) = tokens[0] - assertTrue((small.verify(oldHeader, "POST", "http://x/", oldBody) as Nip98AuthVerifier.Result.Malformed).reason.contains("replay")) - } - } - - @Test - fun withoutReplayRejectionATokenVerifiesAgainButOnlyForItsBody() { - runBlocking { - val reusable = Nip98AuthVerifier(now = { 1_000L }, rejectReplays = false) - val body = "hello".encodeToByteArray() - val (_, header) = signedToken("http://x/", "POST", body) - repeat(3) { assertIs<Nip98AuthVerifier.Result.Verified>(reusable.verify(header, "POST", "http://x/", body)) } - assertIs<Nip98AuthVerifier.Result.Malformed>(reusable.verify(header, "POST", "http://x/", "other".encodeToByteArray())) - } - } } diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt index 1c42b052eb..31695d8da3 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nipFERelayOverHttp/HttpRelayHandlerTest.kt @@ -43,7 +43,6 @@ import com.vitorpamplona.quartz.nip01Core.store.IEventStore import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip77Negentropy.NegentropySettings import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent -import com.vitorpamplona.quartz.nip98HttpAuth.Nip98AuthVerifier import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.runBlocking @@ -364,22 +363,6 @@ class HttpRelayHandlerTest { assertEquals(200, signed.status, signed.lines.toString()) } - @Test - fun aHostsSingleUseVerifierThatIsFullIsA429() { - val h = HttpRelayHandler(MemoryRelay(backend, false), origins = { listOf(origin) }, verifier = Nip98AuthVerifier(maxReplayEntries = 4)) - - // A token's id is its hash, so tokens signed the same second over the same body are one token: - // every one here names its own body. - fun body(n: Int) = """{"kinds":[$n]}""" - val victim = token(HttpRelayCommand.REQ, body(0)) - assertEquals(200, h.ask(HttpRelayCommand.REQ, body(0), victim).status) - for (n in 1..3) assertEquals(200, h.ask(HttpRelayCommand.REQ, body(n), token(HttpRelayCommand.REQ, body(n))).status) - val flooding = h.ask(HttpRelayCommand.REQ, body(4), token(HttpRelayCommand.REQ, body(4))) - assertEquals(429, flooding.status, "a full cache refuses the new token: ${flooding.lines}") - val replayed = h.ask(HttpRelayCommand.REQ, body(0), victim) - assertEquals(401, replayed.status, "and still remembers the old one: ${replayed.lines}") - } - @Test fun aMessageLimitInThePolicyChainStillRuns() { val limited = MemoryRelay(backend, { LimitsPolicy(RelayLimits(maxMessageLength = 4096)) + VerifyPolicy }, limits = null) From de34f392a9012c4f111dd6fb197940a94d160316 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sun, 27 Sep 2026 14:33:13 +0000 Subject: [PATCH 57/60] fix(quartz): a superseded replaceable is REPLACED (OK false), not a duplicate A replaceable/addressable version that a stored one already beats is not written. Since 18c576a068 the SQLite store has reported it as SUPERSEDED, "duplicate: a newer version ...", and RelaySession acks every `duplicate:` reason with OK true. So a client was told its event is on the relay when no REQ will ever return it. NIP-01's third OK field is `true` when the event was accepted. classifyRowError now answers RejectionReason.REPLACED, `replaced: a newer version exists`, which RelaySession sends as OK false. That is strfry's answer too (`false, "replaced: have newer event"`). It also names the case correctly: the relay does not have THIS event, it has a newer one. The W09 work is untouched. Classification still asks the database rather than the driver's message, so Android's null-message constraint failures still classify correctly, and a byte-for-byte re-offer of a stored version is still DUPLICATE (OK true). The MDK `wn` loop 18c576a068 was fixing (a second KeyPackage minted in the same second) now gets `OK false replaced:` instead of raw constraint text, which was the unclassifiable part. Retrying a `replaced:` rejection cannot succeed, so that is for the client to stop doing, not for the relay to paper over with OK true. RejectionReason.SUPERSEDED stays, @Deprecated in favour of REPLACED, for source compatibility. NostrServerTest and InsertOutcomeClassificationTest flip their expectations, and the event-store-semantics skill updates W01/W02 and W09 and adds a changelog entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdSGU88PYrRVyVHjiEc5sw --- .claude/skills/event-store-semantics/SKILL.md | 25 +++++++++++------- .../quartz/nip01Core/store/RejectionReason.kt | 25 +++++++++++++----- .../store/sqlite/SQLiteEventStore.kt | 6 ++--- .../nip01Core/relay/server/NostrServerTest.kt | 26 ++++++++++--------- .../sqlite/InsertOutcomeClassificationTest.kt | 18 ++++++------- 5 files changed, 60 insertions(+), 40 deletions(-) diff --git a/.claude/skills/event-store-semantics/SKILL.md b/.claude/skills/event-store-semantics/SKILL.md index a98f901693..e14608e2fc 100644 --- a/.claude/skills/event-store-semantics/SKILL.md +++ b/.claude/skills/event-store-semantics/SKILL.md @@ -143,11 +143,13 @@ messages quoted below (they surface as the NIP-01 `OK false` reason). kinds. A `BEFORE INSERT` trigger deletes any stored version that is *older* — meaning `created_at` smaller, **or equal `created_at` with lexicographically larger id** (NIP-01 lowest-id-wins). Inserting a version that is *not* newer under that ordering leaves the stored -row in place and fails the unique index → rejected with `RejectionReason.SUPERSEDED` -(`duplicate: a newer version of this replaceable event is already stored`), which the relay -session answers with `OK true` exactly like an id duplicate (NIP-01 `duplicate:` prefix; same -reply nostr-rs-relay gives). Net contract: exactly one version stored; newest wins; ties broken -by lowest id; older re-inserts blocked but acknowledged as already covered. +row in place and fails the unique index → rejected with `RejectionReason.REPLACED` +(`replaced: a newer version exists`), which the relay session answers with `OK false`: the +event was not written, and NIP-01's `true` means accepted. Same reply strfry gives +(`false, "replaced: have newer event"`); nostr-rs-relay answers `true, "duplicate:"` instead, +and `RejectionReason.SUPERSEDED`, which did the same, is deprecated. Net contract: exactly one +version stored; newest wins; ties broken by lowest id; older re-inserts blocked and reported as +such. **STORE-W02 — addressable supersession.** Same as W01 with unique index `(kind, pubkey, d_tag)` over `30000 ≤ kind < 40000`. Nuance: `d_tag` is populated from the @@ -193,16 +195,15 @@ in input order; OK frames pair by event id, not order. exception text.** `SQLiteEventStore.classifyRowError` rolls the row's savepoint back and then asks the connection (which now shows pre-insert state): id already present → `DUPLICATE`; a stored version that beats this one at the replaceable/addressable coordinate (the exact -complement of the supersession predicate in W01/W02) → `SUPERSEDED`; otherwise `Failed`. +complement of the supersession predicate in W01/W02) → `REPLACED`; otherwise `Failed`. Message text is only a fast path and a fallback for trigger RAISEs (`blocked:`, `not allowed`), which leave no database-visible trace. This matters because the message is driver-specific — the bundled JVM driver writes `UNIQUE constraint failed: event_headers.id`, Android's throws an `android.database.SQLException` with a **null** message — so a text-only classifier answered `OK false` on Android for events the store already held. Corollary: re-offering a stored -replaceable/addressable event **byte-for-byte** is `DUPLICATE`, not `SUPERSEDED` (it violates -both indexes and only the id answer is driver-independent); a stale *different* version is -still `SUPERSEDED`. Both carry the `duplicate:` prefix, so the relay reply is `OK true` either -way. +replaceable/addressable event **byte-for-byte** is `DUPLICATE` (`OK true`: it is stored), not +`REPLACED` (it violates both indexes and only the id answer is driver-independent); a stale +*different* version is `REPLACED` (`OK false`: it is not). --- @@ -338,6 +339,10 @@ non-itemizable cases). Add one line per behavior change, newest first: `YYYY-MM-DD <short sha> <rule id> — what changed`. +- 2026-09-27 (pending) W01/W02, W09 — a stale replaceable/addressable version is `REPLACED` + (`replaced:` → `OK false`) again, not `SUPERSEDED` (`duplicate:` → `OK true`): it is not + written, and `OK true` told the client it was. `SUPERSEDED` is deprecated. Byte-for-byte + re-offers stay `DUPLICATE`. - 2026-09-18 (pending) W09, W01/W02 — insert-failure classification now queries the database instead of parsing the driver's exception message (Android's is null, so duplicates were reported as `Failed`/`OK false`). A byte-for-byte re-offer of a stored replaceable/addressable diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/RejectionReason.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/RejectionReason.kt index 00d6ccc7dc..f9b6378e8b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/RejectionReason.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/RejectionReason.kt @@ -51,17 +51,30 @@ object RejectionReason { const val DUPLICATE = "duplicate: already have this event" /** - * A replaceable or addressable event that a stored version already supersedes - * (newer `created_at`, or the same `created_at` and a lower id). Nothing is - * written, and — like [DUPLICATE] — the relay answers `OK true`: NIP-01 keeps - * `duplicate:` as the machine-readable prefix for "already covered", and that - * is what nostr-rs-relay sends here too, so clients that retry on anything - * else (MDK's `wn`) settle instead of re-offering the same event forever. + * No longer produced by any store: a superseded version is [REPLACED]. Its + * `duplicate:` prefix made the relay answer `OK true` for an event that was + * never written, which NIP-01 reserves for an accepted one. */ + @Deprecated( + "A stale replaceable/addressable version is not written, so it is REPLACED (OK false), not a duplicate.", + ReplaceWith("RejectionReason.REPLACED", "com.vitorpamplona.quartz.nip01Core.store.RejectionReason"), + ) const val SUPERSEDED = "duplicate: a newer version of this replaceable event is already stored" const val EXPIRED = "blocked: Cannot insert an expired event" const val DELETED = "blocked: a deletion event exists" const val VANISHED = "blocked: a request to vanish event exists" + + /** + * A replaceable or addressable event that a stored version already supersedes + * (newer `created_at`, or the same `created_at` and a lower id): STORE-W01/W02. + * Nothing is written, so the relay answers `OK false` — NIP-01's third field is + * `true` only when the event was accepted, and acking it would tell the client + * its event is on this relay when no REQ will ever return it. `replaced:` is + * also strfry's answer (`false, "replaced: have newer event"`), and it does not + * misname the case the way `duplicate:` would: the relay does not have THIS + * event, it has a newer one. A client should not retry it; nothing a retry can + * change. A byte-for-byte re-offer of the stored version is [DUPLICATE] instead. + */ const val REPLACED = "replaced: a newer version exists" const val INSERT_FAILED = "error: insert failed" } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt index 9f267c3d6c..a9caa7a1fe 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt @@ -581,9 +581,9 @@ class SQLiteEventStore( } // The replaceable / addressable unique indexes fire only when the supersession // trigger found nothing older to delete, i.e. the stored version already wins - // (STORE-W01/W02). Same shape as a duplicate: nothing to write, `OK true`. + // (STORE-W01/W02). Nothing was written, so REPLACED: `OK false`, not a duplicate. if (message.contains(SUPERSEDED_CONSTRAINT) || isSupersededByStored(event, db)) { - return IEventStore.InsertOutcome.Rejected(RejectionReason.SUPERSEDED) + return IEventStore.InsertOutcome.Rejected(RejectionReason.REPLACED) } return if (message.contains("constraint", ignoreCase = true)) { @@ -618,7 +618,7 @@ class SQLiteEventStore( * trigger *would* have deleted doesn't count. That precision matters * here: this is the fallback for unrecognized failures, and a disk * error while inserting a winning replaceable event must stay `Failed` - * rather than turn into a silent `OK true`. An equal id is the + * rather than be reported as having lost to a stored version. An equal id is the * duplicate case and is answered before this one. */ private fun isSupersededByStored( diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/NostrServerTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/NostrServerTest.kt index bde070947f..a45654e7f2 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/NostrServerTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/NostrServerTest.kt @@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.server.policies.EmptyPolicy import com.vitorpamplona.quartz.nip01Core.relay.server.policies.IRelayPolicy import com.vitorpamplona.quartz.nip01Core.store.IEventStore +import com.vitorpamplona.quartz.nip01Core.store.RejectionReason import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventStore import com.vitorpamplona.quartz.utils.EventFactory import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -151,12 +152,12 @@ class NostrServerTest { /** * STORE-W01: a replaceable event older than the stored version is not written, - * and the relay acknowledges it the way nostr-rs-relay does — `OK true` with the - * NIP-01 `duplicate:` prefix — rather than leaking the unique-index text as a - * rejection the client would keep retrying. + * so the relay answers `OK false` with `replaced:` (strfry's answer) — never + * `OK true`, which NIP-01 keeps for an accepted event, and never the raw + * unique-index text, which no client can classify. */ @Test - fun olderReplaceableIsAcknowledgedAsDuplicateNotRejected() = + fun olderReplaceableIsRejectedAsReplaced() = runTest { val dispatcher = UnconfinedTestDispatcher(testScheduler) val store = EventStore(null) @@ -172,8 +173,8 @@ class NostrServerTest { val okMessages = collector.rawMessagesContaining("OK") assertEquals(2, okMessages.size) assertTrue(okMessages[0].contains(",true,")) - assertTrue(okMessages[1].contains(",true,"), "older version must be acked, got ${okMessages[1]}") - assertTrue(okMessages[1].contains("duplicate:"), "older version must carry the duplicate: prefix") + assertTrue(okMessages[1].contains(",false,"), "older version was not stored, so it must not be acked, got ${okMessages[1]}") + assertTrue(okMessages[1].contains(RejectionReason.PREFIX_REPLACED), "older version must carry the replaced: prefix") val stored = store.query<Event>(Filter(kinds = listOf(0))) assertEquals(listOf(newer.id), stored.map { it.id }, "the newer version stays the only stored one") @@ -183,12 +184,13 @@ class NostrServerTest { /** * STORE-W02 tie: two addressable events with the same `d` tag and the same - * `created_at` — the lower id wins, the other is acknowledged as superseded. - * This is the exact shape MDK's `wn keys publish` produces when it mints a - * second KeyPackage within the same second as the first. + * `created_at` — the lower id wins, the other is rejected as `replaced:`. This + * is the shape MDK's `wn keys publish` produces when it mints a second + * KeyPackage within the same second as the first; the loser is not stored, + * so the client must learn that rather than be told it was accepted. */ @Test - fun sameSecondAddressableTieLoserIsAcknowledgedAsDuplicate() = + fun sameSecondAddressableTieLoserIsRejectedAsReplaced() = runTest { val dispatcher = UnconfinedTestDispatcher(testScheduler) val store = EventStore(null) @@ -204,8 +206,8 @@ class NostrServerTest { val okMessages = collector.rawMessagesContaining("OK") assertEquals(2, okMessages.size) - assertTrue(okMessages[1].contains(",true,"), "tie loser must be acked, got ${okMessages[1]}") - assertTrue(okMessages[1].contains("duplicate:")) + assertTrue(okMessages[1].contains(",false,"), "tie loser was not stored, so it must not be acked, got ${okMessages[1]}") + assertTrue(okMessages[1].contains(RejectionReason.PREFIX_REPLACED)) val stored = store.query<Event>(Filter(kinds = listOf(30443))) assertEquals(listOf(lowerId.id), stored.map { it.id }, "lowest id wins the tie") diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/InsertOutcomeClassificationTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/InsertOutcomeClassificationTest.kt index 2bdcba4dc0..a731bb4d61 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/InsertOutcomeClassificationTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/InsertOutcomeClassificationTest.kt @@ -34,9 +34,9 @@ import kotlin.test.assertTrue /** * `batchInsert` must name *why* a row didn't land, because the relay turns - * that reason into the NIP-01 answer: [RejectionReason.DUPLICATE] and - * [RejectionReason.SUPERSEDED] are `OK true` ("already covered"), everything - * else is `OK false`, which clients retry. + * that reason into the NIP-01 answer: [RejectionReason.DUPLICATE] is `OK true` + * (the event is already here), everything else is `OK false` — including + * [RejectionReason.REPLACED], a stale version that was not written. * * This suite pins the classification **independently of the SQLite driver's * exception text**. The bundled JVM driver raises @@ -81,18 +81,18 @@ class InsertOutcomeClassificationTest : BaseDBTest() { } @Test - fun olderReplaceableIsRejectedAsSuperseded() = + fun olderReplaceableIsRejectedAsReplaced() = forEachDB { db -> val time = TimeUtils.now() val older = signer.sign(MetadataEvent.createNew("Vitor 1", createdAt = time)) val newer = signer.sign(MetadataEvent.createNew("Vitor 2", createdAt = time + 1)) assertEquals(IEventStore.InsertOutcome.Accepted, db.batchInsert(listOf<Event>(newer))[0]) - assertRejected(RejectionReason.SUPERSEDED, db.batchInsert(listOf<Event>(older))[0]) + assertRejected(RejectionReason.REPLACED, db.batchInsert(listOf<Event>(older))[0]) } @Test - fun sameSecondReplaceableTieLoserIsRejectedAsSuperseded() = + fun sameSecondReplaceableTieLoserIsRejectedAsReplaced() = forEachDB { db -> val time = TimeUtils.now() // NIP-01 breaks a created_at tie by lowest id, so the winner is @@ -104,18 +104,18 @@ class InsertOutcomeClassificationTest : BaseDBTest() { ).sortedBy { it.id } assertEquals(IEventStore.InsertOutcome.Accepted, db.batchInsert(listOf<Event>(winner))[0]) - assertRejected(RejectionReason.SUPERSEDED, db.batchInsert(listOf<Event>(loser))[0]) + assertRejected(RejectionReason.REPLACED, db.batchInsert(listOf<Event>(loser))[0]) } @Test - fun olderAddressableIsRejectedAsSuperseded() = + fun olderAddressableIsRejectedAsReplaced() = forEachDB { db -> val time = TimeUtils.now() val older = signer.sign(LongFormContentEvent.build("v1", "title", dTag = "blog", createdAt = time)) val newer = signer.sign(LongFormContentEvent.build("v2", "title", dTag = "blog", createdAt = time + 1)) assertEquals(IEventStore.InsertOutcome.Accepted, db.batchInsert(listOf<Event>(newer))[0]) - assertRejected(RejectionReason.SUPERSEDED, db.batchInsert(listOf<Event>(older))[0]) + assertRejected(RejectionReason.REPLACED, db.batchInsert(listOf<Event>(older))[0]) } @Test From 408faa90c0c1aa292345823cfba78f845adad243 Mon Sep 17 00:00:00 2001 From: vitorpamplona <532031+vitorpamplona@users.noreply.github.com> Date: Sun, 27 Sep 2026 14:41:46 +0000 Subject: [PATCH 58/60] chore: sync Crowdin translations and seed translator npub placeholders --- .../values-hi-rIN/strings.xml | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml b/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml index 9f928a700e..2bd0dcf453 100644 --- a/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values-hi-rIN/strings.xml @@ -5263,11 +5263,47 @@ <string name="cordn_coordinators_title">कोर्डन समायोजक</string> <string name="cordn_coordinators_search_keywords">कोर्डन समायोजक जोडें हटाएँ मिटाएँ स्वास्थ्य सेवासंगणक एमएलएस॰ चर्चा अनुक्रमण</string> <string name="cordn_keypackages_title">कोर्डन कुंचिका पोटलियाँ</string> + <string name="cordn_keypackages_search_keywords">कोर्डन कुंचिका पोटली प्रकाशन आमन्त्रण अन्तिम उपाय समायोजक एमएलएस॰</string> + <string name="cordn_backup_title">कोर्डन सुरक्षित अनुकृति</string> + <string name="cordn_backup_search_keywords">कोर्डन सुरक्षित अनुकृति पुनःस्थापन निर्यात आयात पारणवाक्याम्श समूह पुनःप्राप्ति</string> + <string name="chat_type_cordn_title">कोर्डन समूह</string> <!-- cordn strings on screens main migrated to Res: the shared inbox row and the new-conversation picker. --> + <string name="cordn_group_untitled">समूह %1$s</string> + <string name="cordn_group_no_messages_yet">कोई सन्देश नहीं अब तक</string> + <string name="cordn_group_via_coordinator">कोर्डन समूह। %1$s पर उपलब्ध</string> + <string name="cordn_preview_deleted">सन्देश मिटाया गया</string> + <string name="cordn_preview_voice_note">ध्वनि टीका</string> + <string name="cordn_preview_photo">चित्र</string> + <string name="cordn_preview_video">चलचित्र</string> + <string name="cordn_preview_file">अभिलेख</string> + <string name="new_conversation_cordn_title">कोर्डन समूह</string> + <string name="new_conversation_cordn_chip">समायोजित</string> + <string name="new_conversation_cordn_cta">कोर्डन समूह बनाएँ</string> + <string name="cordn_invitations_entry">क्या आप एक कोर्डन समूह में आमन्त्रित हैं।</string> + <string name="cordn_invitations_entry_action">आमन्त्रण पुनरीक्षण</string> <!-- cordn device migration (spec/applications/multi-device.md §9/§11), as a one-shot handoff rather than continuous sync. The exposure strings say what leaves the device; the fork strings say why the old phone stops. --> + <string name="cordn_hub_title">कोर्डन</string> + <string name="cordn_hub_link">समूह योजक का निरीक्षण</string> + <string name="cordn_migrate_section_send">इस संचारयन्त्र से विस्थापन</string> + <string name="cordn_migrate_section_receive">अन्य संचारयन्त्र से प्राप्त करें</string> + <string name="cordn_keypackages_section">प्रकाशित कुंचिका पोटलियाँ</string> + <string name="cordn_link_section_input">योजक</string> + <string name="cordn_coordinators_section_yours">समायोजक आपके द्वारा उपयुक्त</string> + <string name="cordn_coordinators_section_discover">स्व घोषणा</string> + <string name="cordn_coordinators_section_manual">एक को जोडें स्वयम हाथ से</string> + <string name="cordn_backup_section_passphrase">पारणवाक्याम्श</string> + <string name="cordn_backup_section_export">सुरक्षित अनुकृति बनाएँ</string> + <string name="cordn_backup_section_restore">सुरक्षित अनुकृति से पुनःस्थापन</string> + <string name="cordn_hub_section_service">समायोजक</string> + <string name="cordn_hub_section_device">इस यन्त्र</string> + <string name="cordn_hub_coordinators">समायोजक</string> + <string name="cordn_hub_keypackages">कुंचिका पोटलियाँ</string> + <string name="cordn_hub_backup">सुरक्षित अनुकृति तथा पुनःस्थापन</string> + <string name="cordn_hub_backup_desc">पारणवाक्याम्श द्वारा रहस्यीकृत एक अभिलेख जो आप रखते हैं</string> + <string name="cordn_hub_migrate">अन्य संचारयन्त्र तक विस्थापन</string> <string name="sno_object_title">त्रिमा वस्तु</string> <string name="sno_object_details">%1$d अस्र। %2$d मुख</string> <string name="sno_object_unreadable">इस त्रिमा वस्तु का पठन असफल। नियम %1$s</string> From 2465474d0d894b9a9eeac51eb7d955dc9c2bdffb Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sun, 27 Sep 2026 17:05:19 +0000 Subject: [PATCH 59/60] feat(quartz): NIP-86 event allow list, roles and claims; NIP-43 roles NIP-86 server semantics now follow the spec: banpubkey/banevent drop the entry from the allow list, allowpubkey/allowevent lift the ban, and the un* methods never touch the opposite list. `allowevent` no longer means "unban": it adds the id to a new event allow list (with reason), which BanListPolicy honours by accepting that event without the pubkey/kind checks. An empty event allow list changes nothing. New methods on both client and server: unbanevent, unallowevent, listallowedevents, listdisallowedkinds, createrole/editrole/deleterole, assignrole/unassignrole, listclaims/createclaim/deleteclaim. Role and claim state lives in BanStore and is persisted in geode's state file. Requests now always serialize `params`, even when empty. NIP-43: kind 33534 RelayRoleEvent (label/description/color/order), role ids on kind 13534 member tags, join requests (28934) require the claim and carry the `-` tag, and kind 28935 is deprecated in favour of NIP-86 createclaim. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MGR1u8SyzcUuekub39SBsc --- .../geode/config/RuntimeConfig.kt | 40 +++- .../geode/admin/Nip86EndToEndTest.kt | 33 ++- .../geode/config/RuntimeConfigTest.kt | 42 ++++ .../vitorpamplona/quartz/kinds/KindNames.kt | 2 + .../inviteRequest/RelayInviteRequestEvent.kt | 8 + .../joinRequest/RelayJoinRequestEvent.kt | 20 +- .../list/RelayMembershipListEvent.kt | 15 ++ .../list/TagArrayBuilderExt.kt | 3 + .../nip43RelayMembers/list/TagArrayExt.kt | 5 +- .../nip43RelayMembers/list/tags/MemberTag.kt | 35 ++++ .../nip43RelayMembers/roles/RelayRole.kt | 47 +++++ .../nip43RelayMembers/roles/RelayRoleEvent.kt | 89 ++++++++ .../roles/TagArrayBuilderExt.kt | 35 ++++ .../nip43RelayMembers/roles/TagArrayExt.kt | 36 ++++ .../roles/tags/RoleColorTag.kt | 42 ++++ .../roles/tags/RoleDescriptionTag.kt | 40 ++++ .../roles/tags/RoleLabelTag.kt | 40 ++++ .../roles/tags/RoleOrderTag.kt | 39 ++++ .../nip86RelayManagement/Nip86Client.kt | 66 +++++- .../nip86RelayManagement/rpc/Nip86Method.kt | 20 ++ .../nip86RelayManagement/rpc/Nip86Request.kt | 115 ++++++++++ .../nip86RelayManagement/rpc/Nip86Response.kt | 6 + .../server/BanListPolicy.kt | 12 ++ .../nip86RelayManagement/server/BanStore.kt | 197 ++++++++++++++++-- .../server/Nip86Server.kt | 147 ++++++++++++- .../quartz/utils/EventFactory.kt | 2 + .../nip43RelayMembers/RelayRolesTest.kt | 128 ++++++++++++ .../Nip86RequestResponseTest.kt | 78 +++++++ .../server/BanStoreTest.kt | 149 ++++++++++++- .../server/Nip86ServerTest.kt | 168 ++++++++++++++- 30 files changed, 1627 insertions(+), 32 deletions(-) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/RelayRole.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/RelayRoleEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/TagArrayBuilderExt.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/TagArrayExt.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/tags/RoleColorTag.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/tags/RoleDescriptionTag.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/tags/RoleLabelTag.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/tags/RoleOrderTag.kt create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/RelayRolesTest.kt create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/Nip86RequestResponseTest.kt diff --git a/geode/src/main/kotlin/com/vitorpamplona/geode/config/RuntimeConfig.kt b/geode/src/main/kotlin/com/vitorpamplona/geode/config/RuntimeConfig.kt index 5e0d532424..e861aa7158 100644 --- a/geode/src/main/kotlin/com/vitorpamplona/geode/config/RuntimeConfig.kt +++ b/geode/src/main/kotlin/com/vitorpamplona/geode/config/RuntimeConfig.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.geode.config import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation +import com.vitorpamplona.quartz.nip43RelayMembers.roles.RelayRole import com.vitorpamplona.quartz.nip86RelayManagement.server.BanStore import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @@ -36,7 +37,9 @@ import java.nio.file.StandardCopyOption * - the live NIP-11 info doc (so `changerelayname/description/icon` * survive a restart), and * - the NIP-86 ban / allow / kind lists for pubkeys, events, and - * kinds (NIP-86 admin RPC mutates these directly). + * kinds (NIP-86 admin RPC mutates these directly), and + * - the NIP-43 role definitions, role assignments and invite codes + * managed through the NIP-86 role / claim methods. * * One JSON file per relay. Lives next to the SQLite event store by * convention, but the path is configurable independently via @@ -133,6 +136,33 @@ data class RuntimeConfigData( val bannedEvents: List<BannedEntry> = emptyList(), val allowedKinds: List<Int> = emptyList(), val disallowedKinds: List<Int> = emptyList(), + val allowedEvents: List<BannedEntry> = emptyList(), + val roles: List<RoleEntry> = emptyList(), + val roleAssignments: List<RoleAssignmentEntry> = emptyList(), + val claims: List<String> = emptyList(), +) + +/** A NIP-43 role definition (NIP-86 `createrole` params). */ +@Serializable +data class RoleEntry( + val id: String, + val label: String? = null, + val description: String? = null, + val color: Int? = null, + val order: Int? = null, +) { + fun toRole() = RelayRole(id, label, description, color, order) + + companion object { + fun of(role: RelayRole) = RoleEntry(role.id, role.label, role.description, role.color, role.order) + } +} + +/** The NIP-43 role ids assigned to one pubkey. */ +@Serializable +data class RoleAssignmentEntry( + val pubkey: String, + val roles: List<String>, ) @Serializable @@ -149,6 +179,10 @@ fun RuntimeConfigData.seedInto(banStore: BanStore) { bannedEvents = bannedEvents.map { it.key to it.reason }, allowedKinds = allowedKinds, disallowedKinds = disallowedKinds, + allowedEvents = allowedEvents.map { it.key to it.reason }, + roles = roles.map { it.toRole() }, + roleAssignments = roleAssignments.map { it.pubkey to it.roles }, + claims = claims, ) } @@ -164,4 +198,8 @@ fun snapshotOf( bannedEvents = banStore.listBannedEvents().map { (k, r) -> BannedEntry(k, r) }, allowedKinds = banStore.listAllowedKinds(), disallowedKinds = banStore.listDisallowedKinds(), + allowedEvents = banStore.listAllowedEvents().map { (k, r) -> BannedEntry(k, r) }, + roles = banStore.listRoles().map { RoleEntry.of(it) }, + roleAssignments = banStore.listRoleAssignments().map { (pk, ids) -> RoleAssignmentEntry(pk, ids) }, + claims = banStore.listClaims(), ) diff --git a/geode/src/test/kotlin/com/vitorpamplona/geode/admin/Nip86EndToEndTest.kt b/geode/src/test/kotlin/com/vitorpamplona/geode/admin/Nip86EndToEndTest.kt index 5c119e1cd3..66ef69a46a 100644 --- a/geode/src/test/kotlin/com/vitorpamplona/geode/admin/Nip86EndToEndTest.kt +++ b/geode/src/test/kotlin/com/vitorpamplona/geode/admin/Nip86EndToEndTest.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Request +import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Response import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -39,6 +40,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.boolean import kotlinx.serialization.json.jsonPrimitive @@ -125,7 +127,7 @@ class Nip86EndToEndTest { fun supportedMethodsListsTheServersMethods() { rpc(Nip86Request.supportedMethods(), admin).use { assertEquals(200, it.code) - val json = JsonMapper.fromJson<com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Response>(it.body.string()) + val json = JsonMapper.fromJson<Nip86Response>(it.body.string()) val arr = json.result as JsonArray val names = arr.map { e -> e.jsonPrimitive.content } assertTrue(names.contains("supportedmethods")) @@ -169,7 +171,7 @@ class Nip86EndToEndTest { // Admin bans them. rpc(Nip86Request.banPubkey(targetUser.pubKey, "spam"), admin).use { assertEquals(200, it.code) - val resp = JsonMapper.fromJson<com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Response>(it.body.string()) + val resp = JsonMapper.fromJson<Nip86Response>(it.body.string()) assertEquals(true, (resp.result as JsonPrimitive).boolean) } @@ -178,6 +180,33 @@ class Nip86EndToEndTest { assertEquals(false, after, "BanListPolicy must reject events from banned pubkeys") } + @Test + fun allowEventLetsOneEventPastABanUntilUnallowed() = + runBlocking { + val relayUrl = server.url.normalizeRelayUrl() + rpc(Nip86Request.banPubkey(targetUser.pubKey, "spam"), admin).use { assertEquals(200, it.code) } + + // Admin approves one specific event from the banned author. + val approved = targetUser.sign(TextNoteEvent.build("approved")) + rpc(Nip86Request.allowEvent(approved.id, "reviewed"), admin).use { assertEquals(200, it.code) } + assertEquals(true, nostrClient.publishAndConfirm(approved, setOf(relayUrl)), "allow-listed event bypasses the pubkey ban") + + // Any other event from that author is still blocked. + val other = targetUser.sign(TextNoteEvent.build("other")) + assertEquals(false, nostrClient.publishAndConfirm(other, setOf(relayUrl))) + + rpc(Nip86Request.listAllowedEvents(), admin).use { + val resp = JsonMapper.fromJson<Nip86Response>(it.body.string()) + val ids = (resp.result as JsonArray).map { e -> (e as JsonObject)["id"]!!.jsonPrimitive.content } + assertEquals(listOf(approved.id), ids) + } + + // unallowevent drops the exemption without banning the event. + rpc(Nip86Request.unallowEvent(approved.id), admin).use { assertEquals(200, it.code) } + assertTrue(!relay.banStore.isAllowedEvent(approved.id)) + assertTrue(!relay.banStore.isBannedEvent(approved.id)) + } + @Test fun changeRelayNameFlowsToNip11Endpoint() { rpc(Nip86Request.changeRelayName("renamed-by-admin"), admin).use { diff --git a/geode/src/test/kotlin/com/vitorpamplona/geode/config/RuntimeConfigTest.kt b/geode/src/test/kotlin/com/vitorpamplona/geode/config/RuntimeConfigTest.kt index 10bab8a7dd..04b21cd576 100644 --- a/geode/src/test/kotlin/com/vitorpamplona/geode/config/RuntimeConfigTest.kt +++ b/geode/src/test/kotlin/com/vitorpamplona/geode/config/RuntimeConfigTest.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.geode.RelayEngine import com.vitorpamplona.geode.RelayInfo import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation +import com.vitorpamplona.quartz.nip43RelayMembers.roles.RelayRole import java.io.File import java.nio.file.Files import kotlin.test.AfterTest @@ -124,6 +125,47 @@ class RuntimeConfigTest { } } + @Test + fun eventAllowListRolesAndClaimsSurviveRestart() { + val pk = "b".repeat(64) + val allowedId = "c".repeat(64) + val r1 = relayPersisted() + try { + r1.banStore.allowEvent(allowedId, "approved") + r1.banStore.createRole(RelayRole("mod", label = "Moderator", description = "keeps order", color = 120, order = 2)) + r1.banStore.assignRole(pk, "mod") + r1.banStore.createClaim("invite-123") + } finally { + r1.close() + } + + val r2 = relayPersisted() + try { + assertEquals(listOf(allowedId to "approved"), r2.banStore.listAllowedEvents()) + assertTrue(r2.banStore.isAllowedEvent(allowedId)) + assertEquals(RelayRole("mod", "Moderator", "keeps order", 120, 2), r2.banStore.getRole("mod")) + assertEquals(listOf("mod"), r2.banStore.rolesOf(pk)) + assertEquals(listOf("invite-123"), r2.banStore.listClaims()) + } finally { + r2.close() + } + } + + @Test + fun oldStateFileWithoutNewSectionsStillLoads() { + // A snapshot written before the event allow list / roles / claims existed. + stateFile.writeText("""{"info":{"name":"old"},"bannedEvents":[{"key":"${"d".repeat(64)}","reason":"x"}]}""") + val r = relayPersisted() + try { + assertTrue(r.banStore.isBannedEvent("d".repeat(64))) + assertEquals(emptyList(), r.banStore.listAllowedEvents()) + assertEquals(emptyList(), r.banStore.listRoles()) + assertEquals(emptyList(), r.banStore.listClaims()) + } finally { + r.close() + } + } + @Test fun corruptStateFileIsTolerated() { stateFile.writeText("not valid json {") diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt index b1ff7a9483..6bc8a37c3d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/kinds/KindNames.kt @@ -150,6 +150,7 @@ import com.vitorpamplona.quartz.nip43RelayMembers.joinRequest.RelayJoinRequestEv import com.vitorpamplona.quartz.nip43RelayMembers.leaveRequest.RelayLeaveRequestEvent import com.vitorpamplona.quartz.nip43RelayMembers.list.RelayMembershipListEvent import com.vitorpamplona.quartz.nip43RelayMembers.removeMember.RelayRemoveMemberEvent +import com.vitorpamplona.quartz.nip43RelayMembers.roles.RelayRoleEvent import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent @@ -614,6 +615,7 @@ object KindNames { Ps1SaveEvent.KIND to KindName("PS1 Save", null), NwcInfoEvent.KIND to KindName("NWC Info", "47"), RelayMembershipListEvent.KIND to KindName("Relay Memberships", "43"), + RelayRoleEvent.KIND to KindName("Relay Role", "43"), RootSiteEvent.KIND to KindName("Website Root", "5A"), RootNappletEvent.KIND to KindName("Napplet Root", "5D"), CashuWalletEvent.KIND to KindName("Cashu Wallet", "60"), diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/inviteRequest/RelayInviteRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/inviteRequest/RelayInviteRequestEvent.kt index fde64991fd..90fec26b71 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/inviteRequest/RelayInviteRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/inviteRequest/RelayInviteRequestEvent.kt @@ -27,7 +27,15 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils +/** + * NIP-43 kind 28935: a relay-signed ephemeral event carrying an invite code. + * + * Removed from NIP-43: invite codes are now minted with the NIP-86 + * `createclaim` method ([com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Request.createClaim]). + * Kept so events from relays that still emit it keep parsing. + */ @Immutable +@Deprecated("Removed from NIP-43. Mint invite codes with the NIP-86 `createclaim` method (Nip86Request.createClaim).") class RelayInviteRequestEvent( id: HexKey, pubKey: HexKey, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/joinRequest/RelayJoinRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/joinRequest/RelayJoinRequestEvent.kt index 3c64941867..39ac57af0b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/joinRequest/RelayJoinRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/joinRequest/RelayJoinRequestEvent.kt @@ -24,9 +24,17 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip70ProtectedEvts.protect import com.vitorpamplona.quartz.utils.TimeUtils +/** + * NIP-43 kind 28934: a request to join a relay. Must carry a NIP-70 `-` tag and + * a `claim` tag with the invite code; the relay answers with an `OK`. Invite + * codes are minted by the relay (NIP-86 `createclaim`), not requested with the + * deprecated kind 28935. + */ @Immutable class RelayJoinRequestEvent( id: HexKey, @@ -42,12 +50,16 @@ class RelayJoinRequestEvent( const val KIND = 28934 fun build( - claim: String? = null, + claim: String, createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder<RelayJoinRequestEvent>.() -> Unit = {}, - ) = eventTemplate(KIND, "", createdAt) { - claim?.let { claim(it) } - initializer() + ): EventTemplate<RelayJoinRequestEvent> { + require(claim.isNotBlank()) { "NIP-43 join requests require an invite code (claim)" } + return eventTemplate(KIND, "", createdAt) { + protect() + claim(claim) + initializer() + } } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/list/RelayMembershipListEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/list/RelayMembershipListEvent.kt index 9993cb05b7..6a8188cc5a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/list/RelayMembershipListEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/list/RelayMembershipListEvent.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip43RelayMembers.list.tags.RelayMember import com.vitorpamplona.quartz.nip70ProtectedEvts.protect import com.vitorpamplona.quartz.utils.TimeUtils @@ -39,6 +40,9 @@ class RelayMembershipListEvent( ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { fun members() = tags.members() + /** Members with the role ids (NIP-43 kind 33534 `d` tags) the relay assigned to each. */ + fun membersWithRoles() = tags.membersWithRoles() + companion object { const val KIND = 13534 @@ -51,5 +55,16 @@ class RelayMembershipListEvent( members(members) initializer() } + + /** Like [build], but each member may carry its assigned role ids. */ + fun buildWithRoles( + members: List<RelayMember>, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder<RelayMembershipListEvent>.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + protect() + membersWithRoles(members) + initializer() + } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/list/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/list/TagArrayBuilderExt.kt index d622586deb..f6359a29dc 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/list/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/list/TagArrayBuilderExt.kt @@ -23,5 +23,8 @@ package com.vitorpamplona.quartz.nip43RelayMembers.list import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip43RelayMembers.list.tags.MemberTag +import com.vitorpamplona.quartz.nip43RelayMembers.list.tags.RelayMember fun TagArrayBuilder<RelayMembershipListEvent>.members(pubKeys: List<HexKey>) = addAll(MemberTag.assemble(pubKeys)) + +fun TagArrayBuilder<RelayMembershipListEvent>.membersWithRoles(members: List<RelayMember>) = addAll(members.map { MemberTag.assemble(it) }) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/list/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/list/TagArrayExt.kt index 39da8159b0..d84423c8ec 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/list/TagArrayExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/list/TagArrayExt.kt @@ -21,6 +21,9 @@ package com.vitorpamplona.quartz.nip43RelayMembers.list import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.fastMapNotNullDense import com.vitorpamplona.quartz.nip43RelayMembers.list.tags.MemberTag -fun TagArray.members() = mapNotNull(MemberTag::parse) +fun TagArray.members() = fastMapNotNullDense(MemberTag::parse) + +fun TagArray.membersWithRoles() = fastMapNotNullDense(MemberTag::parseMember) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/list/tags/MemberTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/list/tags/MemberTag.kt index c73004e935..fc93a663ae 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/list/tags/MemberTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/list/tags/MemberTag.kt @@ -20,10 +20,27 @@ */ package com.vitorpamplona.quartz.nip43RelayMembers.list.tags +import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.utils.ensure +/** + * A kind 13534 entry: the member's pubkey plus the NIP-43 role ids (kind + * 33534 `d` tags) assigned to them, in tag order. [roles] is empty for a + * member without roles, which is also what pre-roles relays publish. + */ +@Immutable +data class RelayMember( + val pubKey: HexKey, + val roles: List<String> = emptyList(), +) + +/** + * NIP-43 `["member", <pubkey>, <role-id>...]`. Role ids after the pubkey are + * optional: [parse] ignores them (backward compatible) and [parseMember] + * returns them. + */ class MemberTag { companion object { const val TAG_NAME = "member" @@ -35,8 +52,26 @@ class MemberTag { return tag[1] } + fun parseMember(tag: Array<String>): RelayMember? { + val pubKey = parse(tag) ?: return null + if (tag.size <= 2) return RelayMember(pubKey) + val roles = ArrayList<String>(tag.size - 2) + for (i in 2 until tag.size) { + val role = tag[i] + if (role.isNotEmpty() && role !in roles) roles.add(role) + } + return RelayMember(pubKey, roles) + } + fun assemble(pubKey: HexKey) = arrayOf(TAG_NAME, pubKey) + fun assemble( + pubKey: HexKey, + roles: List<String>, + ): Array<String> = arrayOf(TAG_NAME, pubKey) + roles + + fun assemble(member: RelayMember) = assemble(member.pubKey, member.roles) + fun assemble(pubKeys: List<HexKey>) = pubKeys.map { assemble(it) } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/RelayRole.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/RelayRole.kt new file mode 100644 index 0000000000..59018cb2eb --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/RelayRole.kt @@ -0,0 +1,47 @@ +/* + * 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.quartz.nip43RelayMembers.roles + +import androidx.compose.runtime.Immutable + +/** + * A NIP-43 role as the relay defines it: the content of a kind 33534 + * [RelayRoleEvent], and the `[id, label, description, color, order]` params + * of the NIP-86 `createrole` / `editrole` methods. + * + * [color] is a hue in `0..360` (see [isValidHue]); [order] is a display-only + * sort key. Everything but [id] is optional. + */ +@Immutable +data class RelayRole( + val id: String, + val label: String? = null, + val description: String? = null, + val color: Int? = null, + val order: Int? = null, +) { + companion object { + const val MIN_HUE = 0 + const val MAX_HUE = 360 + + fun isValidHue(hue: Int) = hue in MIN_HUE..MAX_HUE + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/RelayRoleEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/RelayRoleEvent.kt new file mode 100644 index 0000000000..26c3193092 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/RelayRoleEvent.kt @@ -0,0 +1,89 @@ +/* + * 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.quartz.nip43RelayMembers.roles + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag +import com.vitorpamplona.quartz.nip70ProtectedEvts.protect +import com.vitorpamplona.quartz.utils.TimeUtils + +/** + * NIP-43 kind 33534: a role the relay defines and may assign to members. + * + * Signed by the relay's NIP-11 `self` pubkey and protected (NIP-70 `-` tag). + * The `d` tag is the role id — the value a kind 13534 `member` tag lists after + * the pubkey. `label`, `description`, `color` (a hue, 0..360) and `order` + * (display-only sort key) are optional. + */ +@Immutable +class RelayRoleEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array<Array<String>>, + content: String, + sig: HexKey, +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun roleId() = dTag() + + fun label() = tags.roleLabel() + + fun description() = tags.roleDescription() + + /** Hue in `0..360`, or null when absent or out of range. */ + fun color() = tags.roleColor() + + fun order() = tags.roleOrder() + + fun role() = + RelayRole( + id = roleId(), + label = label(), + description = description(), + color = color(), + order = order(), + ) + + companion object { + const val KIND = 33534 + + fun build( + role: RelayRole, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder<RelayRoleEvent>.() -> Unit = {}, + ) = eventTemplate<RelayRoleEvent>(KIND, "", createdAt) { + protect() + dTag(role.id) + role.label?.let { roleLabel(it) } + role.description?.let { roleDescription(it) } + role.color?.let { + require(RelayRole.isValidHue(it)) { "role color must be a hue between 0 and 360, got $it" } + roleColor(it) + } + role.order?.let { roleOrder(it) } + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..40f1bdd0b0 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/TagArrayBuilderExt.kt @@ -0,0 +1,35 @@ +/* + * 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.quartz.nip43RelayMembers.roles + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip43RelayMembers.roles.tags.RoleColorTag +import com.vitorpamplona.quartz.nip43RelayMembers.roles.tags.RoleDescriptionTag +import com.vitorpamplona.quartz.nip43RelayMembers.roles.tags.RoleLabelTag +import com.vitorpamplona.quartz.nip43RelayMembers.roles.tags.RoleOrderTag + +fun TagArrayBuilder<RelayRoleEvent>.roleLabel(label: String) = addUnique(RoleLabelTag.assemble(label)) + +fun TagArrayBuilder<RelayRoleEvent>.roleDescription(description: String) = addUnique(RoleDescriptionTag.assemble(description)) + +fun TagArrayBuilder<RelayRoleEvent>.roleColor(hue: Int) = addUnique(RoleColorTag.assemble(hue)) + +fun TagArrayBuilder<RelayRoleEvent>.roleOrder(order: Int) = addUnique(RoleOrderTag.assemble(order)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/TagArrayExt.kt new file mode 100644 index 0000000000..38dceef188 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/TagArrayExt.kt @@ -0,0 +1,36 @@ +/* + * 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.quartz.nip43RelayMembers.roles + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.fastFirstNotNullOfOrNull +import com.vitorpamplona.quartz.nip43RelayMembers.roles.tags.RoleColorTag +import com.vitorpamplona.quartz.nip43RelayMembers.roles.tags.RoleDescriptionTag +import com.vitorpamplona.quartz.nip43RelayMembers.roles.tags.RoleLabelTag +import com.vitorpamplona.quartz.nip43RelayMembers.roles.tags.RoleOrderTag + +fun TagArray.roleLabel() = fastFirstNotNullOfOrNull(RoleLabelTag::parse) + +fun TagArray.roleDescription() = fastFirstNotNullOfOrNull(RoleDescriptionTag::parse) + +fun TagArray.roleColor() = fastFirstNotNullOfOrNull(RoleColorTag::parse) + +fun TagArray.roleOrder() = fastFirstNotNullOfOrNull(RoleOrderTag::parse) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/tags/RoleColorTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/tags/RoleColorTag.kt new file mode 100644 index 0000000000..719e6b4ab4 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/tags/RoleColorTag.kt @@ -0,0 +1,42 @@ +/* + * 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.quartz.nip43RelayMembers.roles.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip43RelayMembers.roles.RelayRole +import com.vitorpamplona.quartz.utils.ensure + +/** NIP-43 kind 33534 `color` tag: a hue from 0 to 360. Non-numeric or out-of-range values parse as null. */ +class RoleColorTag { + companion object { + const val TAG_NAME = "color" + + fun parse(tag: Array<String>): Int? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + val hue = tag[1].trim().toIntOrNull() ?: return null + ensure(RelayRole.isValidHue(hue)) { return null } + return hue + } + + fun assemble(hue: Int) = arrayOf(TAG_NAME, hue.toString()) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/tags/RoleDescriptionTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/tags/RoleDescriptionTag.kt new file mode 100644 index 0000000000..176b26773d --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/tags/RoleDescriptionTag.kt @@ -0,0 +1,40 @@ +/* + * 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.quartz.nip43RelayMembers.roles.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** NIP-43 kind 33534 `description` tag. */ +class RoleDescriptionTag { + companion object { + const val TAG_NAME = "description" + + fun parse(tag: Array<String>): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble(description: String) = arrayOf(TAG_NAME, description) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/tags/RoleLabelTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/tags/RoleLabelTag.kt new file mode 100644 index 0000000000..350b05d65c --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/tags/RoleLabelTag.kt @@ -0,0 +1,40 @@ +/* + * 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.quartz.nip43RelayMembers.roles.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** NIP-43 kind 33534 `label` tag: the role's display name. */ +class RoleLabelTag { + companion object { + const val TAG_NAME = "label" + + fun parse(tag: Array<String>): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble(label: String) = arrayOf(TAG_NAME, label) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/tags/RoleOrderTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/tags/RoleOrderTag.kt new file mode 100644 index 0000000000..00db5227bd --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/roles/tags/RoleOrderTag.kt @@ -0,0 +1,39 @@ +/* + * 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.quartz.nip43RelayMembers.roles.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** NIP-43 kind 33534 `order` tag: a display-only integer sort key. */ +class RoleOrderTag { + companion object { + const val TAG_NAME = "order" + + fun parse(tag: Array<String>): Int? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + return tag[1].trim().toIntOrNull() + } + + fun assemble(order: Int) = arrayOf(TAG_NAME, order.toString()) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/Nip86Client.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/Nip86Client.kt index 3685bb11bb..00b9a24c00 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/Nip86Client.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/Nip86Client.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.core.JsonMapper import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip86RelayManagement.rpc.AllowedEvent import com.vitorpamplona.quartz.nip86RelayManagement.rpc.AllowedPubkey import com.vitorpamplona.quartz.nip86RelayManagement.rpc.BannedEvent import com.vitorpamplona.quartz.nip86RelayManagement.rpc.BannedPubkey @@ -84,16 +85,31 @@ class Nip86Client( return JsonMapper.fromJson<List<BannedEvent>>(result.toString()) } + fun parseAllowedEvents(response: Nip86Response): List<AllowedEvent>? { + val result = response.result ?: return null + return JsonMapper.fromJson<List<AllowedEvent>>(result.toString()) + } + fun parseEventsNeedingModeration(response: Nip86Response): List<EventNeedingModeration>? { val result = response.result ?: return null return JsonMapper.fromJson<List<EventNeedingModeration>>(result.toString()) } - fun parseAllowedKinds(response: Nip86Response): List<Int>? { + fun parseAllowedKinds(response: Nip86Response): List<Int>? = parseKinds(response) + + fun parseDisallowedKinds(response: Nip86Response): List<Int>? = parseKinds(response) + + private fun parseKinds(response: Nip86Response): List<Int>? { val result = response.result ?: return null return (result as? JsonArray)?.map { it.jsonPrimitive.int } } + /** `listclaims` result: a plain array of NIP-43 invite codes. */ + fun parseClaims(response: Nip86Response): List<String>? { + val result = response.result ?: return null + return (result as? JsonArray)?.map { it.jsonPrimitive.content } + } + fun parseBlockedIps(response: Nip86Response): List<BlockedIp>? { val result = response.result ?: return null return JsonMapper.fromJson<List<BlockedIp>>(result.toString()) @@ -134,13 +150,59 @@ class Nip86Client( reason: String? = null, ) = Nip86Request.allowEvent(eventId, reason) + fun unallowEventRequest( + eventId: String, + reason: String? = null, + ) = Nip86Request.unallowEvent(eventId, reason) + fun banEventRequest( eventId: String, reason: String? = null, ) = Nip86Request.banEvent(eventId, reason) + fun unbanEventRequest( + eventId: String, + reason: String? = null, + ) = Nip86Request.unbanEvent(eventId, reason) + fun listBannedEventsRequest() = Nip86Request.listBannedEvents() + fun listAllowedEventsRequest() = Nip86Request.listAllowedEvents() + + fun createRoleRequest( + id: String, + label: String? = null, + description: String? = null, + color: Int? = null, + order: Int? = null, + ) = Nip86Request.createRole(id, label, description, color, order) + + fun editRoleRequest( + id: String, + label: String? = null, + description: String? = null, + color: Int? = null, + order: Int? = null, + ) = Nip86Request.editRole(id, label, description, color, order) + + fun deleteRoleRequest(id: String) = Nip86Request.deleteRole(id) + + fun assignRoleRequest( + pubkey: String, + roleId: String, + ) = Nip86Request.assignRole(pubkey, roleId) + + fun unassignRoleRequest( + pubkey: String, + roleId: String, + ) = Nip86Request.unassignRole(pubkey, roleId) + + fun listClaimsRequest() = Nip86Request.listClaims() + + fun createClaimRequest(claim: String) = Nip86Request.createClaim(claim) + + fun deleteClaimRequest(claim: String) = Nip86Request.deleteClaim(claim) + fun changeRelayNameRequest(newName: String) = Nip86Request.changeRelayName(newName) fun changeRelayDescriptionRequest(newDescription: String) = Nip86Request.changeRelayDescription(newDescription) @@ -153,6 +215,8 @@ class Nip86Client( fun listAllowedKindsRequest() = Nip86Request.listAllowedKinds() + fun listDisallowedKindsRequest() = Nip86Request.listDisallowedKinds() + fun blockIpRequest( ip: String, reason: String? = null, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/rpc/Nip86Method.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/rpc/Nip86Method.kt index a52f48be72..41cb4ac81a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/rpc/Nip86Method.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/rpc/Nip86Method.kt @@ -28,16 +28,36 @@ object Nip86Method { const val ALLOW_PUBKEY = "allowpubkey" const val UNALLOW_PUBKEY = "unallowpubkey" const val LIST_ALLOWED_PUBKEYS = "listallowedpubkeys" + const val CREATE_ROLE = "createrole" + const val EDIT_ROLE = "editrole" + const val DELETE_ROLE = "deleterole" + const val ASSIGN_ROLE = "assignrole" + const val UNASSIGN_ROLE = "unassignrole" + const val LIST_CLAIMS = "listclaims" + const val CREATE_CLAIM = "createclaim" + const val DELETE_CLAIM = "deleteclaim" const val LIST_EVENTS_NEEDING_MODERATION = "listeventsneedingmoderation" + + /** Adds an event to the relay's allow list (and removes it from the ban list). */ const val ALLOW_EVENT = "allowevent" + + /** Removes an event from the allow list without banning it. */ + const val UNALLOW_EVENT = "unallowevent" + + /** Bans an event (and removes it from the allow list). */ const val BAN_EVENT = "banevent" + + /** Removes an event from the ban list without allow-listing it. */ + const val UNBAN_EVENT = "unbanevent" const val LIST_BANNED_EVENTS = "listbannedevents" + const val LIST_ALLOWED_EVENTS = "listallowedevents" const val CHANGE_RELAY_NAME = "changerelayname" const val CHANGE_RELAY_DESCRIPTION = "changerelaydescription" const val CHANGE_RELAY_ICON = "changerelayicon" const val ALLOW_KIND = "allowkind" const val DISALLOW_KIND = "disallowkind" const val LIST_ALLOWED_KINDS = "listallowedkinds" + const val LIST_DISALLOWED_KINDS = "listdisallowedkinds" const val BLOCK_IP = "blockip" const val UNBLOCK_IP = "unblockip" const val LIST_BLOCKED_IPS = "listblockedips" diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/rpc/Nip86Request.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/rpc/Nip86Request.kt index 1260c0ad5e..c181927780 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/rpc/Nip86Request.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/rpc/Nip86Request.kt @@ -20,14 +20,20 @@ */ package com.vitorpamplona.quartz.nip86RelayManagement.rpc +import kotlinx.serialization.EncodeDefault +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonArray @Serializable class Nip86Request( val method: String, + // NIP-86 requests always carry `params`, even when empty (`[]`). + @OptIn(ExperimentalSerializationApi::class) + @EncodeDefault val params: JsonArray = JsonArray(emptyList()), ) { companion object { @@ -78,6 +84,74 @@ class Nip86Request( method = Nip86Method.LIST_ALLOWED_PUBKEYS, ) + /** + * NIP-86 `createrole`: `[id, label, description, color, order]`. Every + * position is always sent so the relay can read them positionally; a + * missing optional value goes out as JSON `null`. [color] is a hue + * (0..360) and [order] a display-only sort key, both sent as numbers. + */ + fun createRole( + id: String, + label: String? = null, + description: String? = null, + color: Int? = null, + order: Int? = null, + ) = Nip86Request( + method = Nip86Method.CREATE_ROLE, + params = roleParams(id, label, description, color, order), + ) + + /** NIP-86 `editrole`: same `[id, label, description, color, order]` shape as [createRole]. */ + fun editRole( + id: String, + label: String? = null, + description: String? = null, + color: Int? = null, + order: Int? = null, + ) = Nip86Request( + method = Nip86Method.EDIT_ROLE, + params = roleParams(id, label, description, color, order), + ) + + fun deleteRole(id: String) = + Nip86Request( + method = Nip86Method.DELETE_ROLE, + params = buildJsonArray { add(JsonPrimitive(id)) }, + ) + + fun assignRole( + pubkey: String, + roleId: String, + ) = Nip86Request( + method = Nip86Method.ASSIGN_ROLE, + params = buildParams(pubkey, roleId), + ) + + fun unassignRole( + pubkey: String, + roleId: String, + ) = Nip86Request( + method = Nip86Method.UNASSIGN_ROLE, + params = buildParams(pubkey, roleId), + ) + + fun listClaims() = + Nip86Request( + method = Nip86Method.LIST_CLAIMS, + ) + + fun createClaim(claim: String) = + Nip86Request( + method = Nip86Method.CREATE_CLAIM, + params = buildJsonArray { add(JsonPrimitive(claim)) }, + ) + + fun deleteClaim(claim: String) = + Nip86Request( + method = Nip86Method.DELETE_CLAIM, + params = buildJsonArray { add(JsonPrimitive(claim)) }, + ) + fun listEventsNeedingModeration() = Nip86Request( method = Nip86Method.LIST_EVENTS_NEEDING_MODERATION, @@ -91,6 +165,14 @@ class Nip86Request( params = buildParams(eventId, reason), ) + fun unallowEvent( + eventId: String, + reason: String? = null, + ) = Nip86Request( + method = Nip86Method.UNALLOW_EVENT, + params = buildParams(eventId, reason), + ) + fun banEvent( eventId: String, reason: String? = null, @@ -99,11 +181,24 @@ class Nip86Request( params = buildParams(eventId, reason), ) + fun unbanEvent( + eventId: String, + reason: String? = null, + ) = Nip86Request( + method = Nip86Method.UNBAN_EVENT, + params = buildParams(eventId, reason), + ) + fun listBannedEvents() = Nip86Request( method = Nip86Method.LIST_BANNED_EVENTS, ) + fun listAllowedEvents() = + Nip86Request( + method = Nip86Method.LIST_ALLOWED_EVENTS, + ) + fun changeRelayName(newName: String) = Nip86Request( method = Nip86Method.CHANGE_RELAY_NAME, @@ -139,6 +234,11 @@ class Nip86Request( method = Nip86Method.LIST_ALLOWED_KINDS, ) + fun listDisallowedKinds() = + Nip86Request( + method = Nip86Method.LIST_DISALLOWED_KINDS, + ) + fun blockIp( ip: String, reason: String? = null, @@ -158,6 +258,21 @@ class Nip86Request( method = Nip86Method.LIST_BLOCKED_IPS, ) + private fun roleParams( + id: String, + label: String?, + description: String?, + color: Int?, + order: Int?, + ): JsonArray = + buildJsonArray { + add(JsonPrimitive(id)) + add(label?.let { JsonPrimitive(it) } ?: JsonNull) + add(description?.let { JsonPrimitive(it) } ?: JsonNull) + add(color?.let { JsonPrimitive(it) } ?: JsonNull) + add(order?.let { JsonPrimitive(it) } ?: JsonNull) + } + private fun buildParams( primary: String, reason: String? = null, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/rpc/Nip86Response.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/rpc/Nip86Response.kt index 8f34cb275e..09642d7a43 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/rpc/Nip86Response.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/rpc/Nip86Response.kt @@ -47,6 +47,12 @@ class BannedEvent( val reason: String? = null, ) +@Serializable +class AllowedEvent( + val id: String, + val reason: String? = null, +) + @Serializable class EventNeedingModeration( val id: String, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/server/BanListPolicy.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/server/BanListPolicy.kt index dd693e80e1..aed433d44a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/server/BanListPolicy.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/server/BanListPolicy.kt @@ -30,6 +30,15 @@ import com.vitorpamplona.quartz.nip01Core.relay.server.policies.PolicyResult * non-empty pubkey allow list, or kind disallowed / not in the kind * allow list. * + * An event id on the NIP-86 event allow list (`allowevent`) is an + * explicit, per-event operator approval, so it is accepted without + * consulting the pubkey and kind rules — the most specific decision + * wins, the same way `banevent` rejects an event from an otherwise + * allowed author. The event allow list never restricts anything: with + * it empty (the default) this policy behaves exactly as before. It only + * short-circuits this policy; other policies stacked next to it still + * apply. + * * Functionally equivalent to (and a superset of) the static * [com.vitorpamplona.quartz.nip01Core.relay.server.policies.KindAllowDenyPolicy] + * [com.vitorpamplona.quartz.nip01Core.relay.server.policies.PubkeyAllowDenyPolicy]. @@ -47,6 +56,9 @@ class BanListPolicy( if (banStore.isBannedEvent(ev.id)) { return PolicyResult.Rejected("blocked: event id is banned") } + if (banStore.isAllowedEvent(ev.id)) { + return PolicyResult.Accepted(cmd) + } if (banStore.isBanned(ev.pubKey)) { return PolicyResult.Rejected("blocked: pubkey is banned") } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/server/BanStore.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/server/BanStore.kt index ba38eff154..b2ef70e00f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/server/BanStore.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/server/BanStore.kt @@ -21,17 +21,26 @@ package com.vitorpamplona.quartz.nip86RelayManagement.server import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip43RelayMembers.roles.RelayRole import kotlin.concurrent.atomics.AtomicReference import kotlin.concurrent.atomics.ExperimentalAtomicApi /** * Lock-free runtime state for the NIP-86 management API. Holds the * ban/allow lists that [BanListPolicy] consults on every accept call, - * plus an [onMutation] hook so the relay can persist the latest - * snapshot whenever an admin RPC mutates state. + * the NIP-43 role definitions / assignments and invite codes (claims) + * that the role and claim RPCs manage, plus an [onMutation] hook so the + * relay can persist the latest snapshot whenever an admin RPC mutates + * state. * - * Each entry carries an optional reason string so list-* RPCs can echo - * back why an admin took the action — useful for audit trails. + * Each list entry carries an optional reason string so list-* RPCs can + * echo back why an admin took the action — useful for audit trails. + * + * Ban / allow lists follow NIP-86: `ban*` removes the entry from the + * matching allow list and `allow*` removes it from the ban list, while + * `unban*` / `unallow*` only clear their own list — they never add the + * entry to the opposite one. So a pubkey or event id is never on both + * lists at once. * * Persistence is intentionally NOT inside this class; supply * [onMutation] to flush to disk (or wherever) and use [seedFromSnapshot] @@ -47,18 +56,21 @@ class BanStore( private val onMutation: (() -> Unit)? = null, ) { /** - * Single immutable snapshot of all ban/allow state. Combined into - * one object so kind allow/disallow lock-step (allow adds to - * allowedKinds AND removes from disallowedKinds) is naturally - * atomic — no possibility of an interleaved reader observing a - * kind in both sets. + * Single immutable snapshot of all state. Combined into one object so + * the lock-step moves (ban removes from allow, allow removes from + * ban, deleting a role unassigns it) are naturally atomic — no + * interleaved reader can observe an entry on both lists. */ private data class State( val bannedPubkeys: Map<HexKey, String?> = emptyMap(), val allowedPubkeys: Map<HexKey, String?> = emptyMap(), val bannedEventIds: Map<HexKey, String?> = emptyMap(), + val allowedEventIds: Map<HexKey, String?> = emptyMap(), val allowedKinds: Set<Int> = emptySet(), val disallowedKinds: Set<Int> = emptySet(), + val roles: Map<String, RelayRole> = emptyMap(), + val memberRoles: Map<HexKey, List<String>> = emptyMap(), + val claims: Set<String> = emptySet(), ) private val state = AtomicReference(State()) @@ -71,13 +83,33 @@ class BanStore( onMutation?.invoke() } + /** + * Like [mutate], but [transform] may return `null` to leave the state + * untouched (e.g. editing a role that doesn't exist). Returns whether + * the state changed; [onMutation] only fires when it did. + */ + private inline fun mutateIf(transform: (State) -> State?): Boolean { + while (true) { + val current = state.load() + val next = transform(current) ?: return false + if (state.compareAndSet(current, next)) break + } + onMutation?.invoke() + return true + } + // -- Pubkey ban list ----------------------------------------------------- + /** Bans [pubkey] and, per NIP-86, drops it from the allow list. */ fun banPubkey( pubkey: HexKey, reason: String? = null, - ) = mutate { it.copy(bannedPubkeys = it.bannedPubkeys + (pubkey.lowercase() to reason)) } + ) = mutate { + val pk = pubkey.lowercase() + it.copy(bannedPubkeys = it.bannedPubkeys + (pk to reason), allowedPubkeys = it.allowedPubkeys - pk) + } + /** Lifts a ban. Does not add [pubkey] to the allow list. */ fun unbanPubkey(pubkey: HexKey) = mutate { it.copy(bannedPubkeys = it.bannedPubkeys - pubkey.lowercase()) } fun isBanned(pubkey: HexKey): Boolean = pubkey.lowercase() in state.load().bannedPubkeys @@ -90,11 +122,16 @@ class BanStore( // -- Pubkey allow list --------------------------------------------------- + /** Allow-lists [pubkey] and, per NIP-86, drops it from the ban list. */ fun allowPubkey( pubkey: HexKey, reason: String? = null, - ) = mutate { it.copy(allowedPubkeys = it.allowedPubkeys + (pubkey.lowercase() to reason)) } + ) = mutate { + val pk = pubkey.lowercase() + it.copy(allowedPubkeys = it.allowedPubkeys + (pk to reason), bannedPubkeys = it.bannedPubkeys - pk) + } + /** Removes [pubkey] from the allow list. Does not ban it. */ fun unallowPubkey(pubkey: HexKey) = mutate { it.copy(allowedPubkeys = it.allowedPubkeys - pubkey.lowercase()) } fun isAllowedPubkey(pubkey: HexKey): Boolean = pubkey.lowercase() in state.load().allowedPubkeys @@ -107,24 +144,52 @@ class BanStore( fun hasAllowList(): Boolean = state.load().allowedPubkeys.isNotEmpty() - // -- Event id ban list --------------------------------------------------- + // -- Event id ban / allow lists ----------------------------------------- + /** Bans [eventId] and, per NIP-86, drops it from the event allow list. */ fun banEvent( eventId: HexKey, reason: String? = null, - ) = mutate { it.copy(bannedEventIds = it.bannedEventIds + (eventId.lowercase() to reason)) } + ) = mutate { + val id = eventId.lowercase() + it.copy(bannedEventIds = it.bannedEventIds + (id to reason), allowedEventIds = it.allowedEventIds - id) + } - /** Removes an event id from the ban list. Mirrors NIP-86 `allowevent`. */ - fun allowEvent(eventId: HexKey) = mutate { it.copy(bannedEventIds = it.bannedEventIds - eventId.lowercase()) } + /** NIP-86 `unbanevent`: removes [eventId] from the ban list without allow-listing it. */ + fun unbanEvent(eventId: HexKey) = mutate { it.copy(bannedEventIds = it.bannedEventIds - eventId.lowercase()) } + + /** + * NIP-86 `allowevent`: adds [eventId] to the event allow list and drops + * it from the ban list. See [BanListPolicy] for what an allow-listed + * event is exempt from. + */ + fun allowEvent( + eventId: HexKey, + reason: String? = null, + ) = mutate { + val id = eventId.lowercase() + it.copy(allowedEventIds = it.allowedEventIds + (id to reason), bannedEventIds = it.bannedEventIds - id) + } + + /** NIP-86 `unallowevent`: removes [eventId] from the allow list without banning it. */ + fun unallowEvent(eventId: HexKey) = mutate { it.copy(allowedEventIds = it.allowedEventIds - eventId.lowercase()) } fun isBannedEvent(eventId: HexKey): Boolean = eventId.lowercase() in state.load().bannedEventIds + fun isAllowedEvent(eventId: HexKey): Boolean = eventId.lowercase() in state.load().allowedEventIds + fun listBannedEvents(): List<Pair<HexKey, String?>> = state .load() .bannedEventIds.entries .map { it.key to it.value } + fun listAllowedEvents(): List<Pair<HexKey, String?>> = + state + .load() + .allowedEventIds.entries + .map { it.key to it.value } + // -- Kind allow / deny -------------------------------------------------- /** @@ -160,12 +225,94 @@ class BanStore( return kind in s.allowedKinds } + // -- NIP-43 roles ------------------------------------------------------- + + /** NIP-86 `createrole`. Returns false (and changes nothing) if a role with that id exists. */ + fun createRole(role: RelayRole): Boolean = + mutateIf { + if (role.id in it.roles) null else it.copy(roles = it.roles + (role.id to role)) + } + + /** NIP-86 `editrole`. Returns false (and changes nothing) if no role has that id. */ + fun editRole(role: RelayRole): Boolean = + mutateIf { + if (role.id !in it.roles) null else it.copy(roles = it.roles + (role.id to role)) + } + + /** NIP-86 `deleterole`. Also unassigns the role from every member. Idempotent. */ + fun deleteRole(roleId: String) = + mutate { s -> + s.copy( + roles = s.roles - roleId, + memberRoles = + s.memberRoles + .mapValues { (_, roles) -> roles - roleId } + .filterValues { it.isNotEmpty() }, + ) + } + + fun getRole(roleId: String): RelayRole? = state.load().roles[roleId] + + /** Roles sorted by their display [RelayRole.order] (unordered last), then id. */ + fun listRoles(): List<RelayRole> = + state + .load() + .roles.values + .sortedWith(compareBy<RelayRole>({ it.order ?: Int.MAX_VALUE }, { it.id })) + + /** NIP-86 `assignrole`. Returns false (and changes nothing) if the role doesn't exist. */ + fun assignRole( + pubkey: HexKey, + roleId: String, + ): Boolean = + mutateIf { s -> + if (roleId !in s.roles) return@mutateIf null + val pk = pubkey.lowercase() + val current = s.memberRoles[pk].orEmpty() + if (roleId in current) s else s.copy(memberRoles = s.memberRoles + (pk to current + roleId)) + } + + /** NIP-86 `unassignrole`. Idempotent. */ + fun unassignRole( + pubkey: HexKey, + roleId: String, + ) = mutate { s -> + val pk = pubkey.lowercase() + val remaining = s.memberRoles[pk].orEmpty() - roleId + s.copy(memberRoles = if (remaining.isEmpty()) s.memberRoles - pk else s.memberRoles + (pk to remaining)) + } + + fun rolesOf(pubkey: HexKey): List<String> = state.load().memberRoles[pubkey.lowercase()].orEmpty() + + /** Every pubkey with at least one role, with its role ids in assignment order. */ + fun listRoleAssignments(): List<Pair<HexKey, List<String>>> = + state + .load() + .memberRoles.entries + .map { it.key to it.value } + + // -- NIP-43 invite codes (claims) --------------------------------------- + + /** NIP-86 `createclaim`. Idempotent. */ + fun createClaim(claim: String) = mutate { it.copy(claims = it.claims + claim) } + + /** NIP-86 `deleteclaim`. Idempotent. */ + fun deleteClaim(claim: String) = mutate { it.copy(claims = it.claims - claim) } + + /** True when [claim] is an invite code the relay currently accepts. */ + fun isValidClaim(claim: String): Boolean = claim in state.load().claims + + fun listClaims(): List<String> = state.load().claims.toList() + /** * Bulk-load state without firing [onMutation]. Used at startup to * seed the in-memory state from a persisted snapshot — we don't * want every individual `put` to trigger another disk write. After * this call the store behaves exactly as if every entry had been * mutated through the public API. + * + * A snapshot that lists an id on both a ban and an allow list (only + * possible from a hand-edited file) keeps the ban and drops the allow. */ fun seedFromSnapshot( bannedPubkeys: List<Pair<HexKey, String?>> = emptyList(), @@ -173,14 +320,28 @@ class BanStore( bannedEvents: List<Pair<HexKey, String?>> = emptyList(), allowedKinds: List<Int> = emptyList(), disallowedKinds: List<Int> = emptyList(), + allowedEvents: List<Pair<HexKey, String?>> = emptyList(), + roles: List<RelayRole> = emptyList(), + roleAssignments: List<Pair<HexKey, List<String>>> = emptyList(), + claims: List<String> = emptyList(), ) { + val banned = bannedPubkeys.associate { (k, r) -> k.lowercase() to r } + val bannedEv = bannedEvents.associate { (k, r) -> k.lowercase() to r } + val roleMap = roles.associateBy { it.id } state.store( State( - bannedPubkeys = bannedPubkeys.associate { (k, r) -> k.lowercase() to r }, - allowedPubkeys = allowedPubkeys.associate { (k, r) -> k.lowercase() to r }, - bannedEventIds = bannedEvents.associate { (k, r) -> k.lowercase() to r }, + bannedPubkeys = banned, + allowedPubkeys = allowedPubkeys.associate { (k, r) -> k.lowercase() to r } - banned.keys, + bannedEventIds = bannedEv, + allowedEventIds = allowedEvents.associate { (k, r) -> k.lowercase() to r } - bannedEv.keys, allowedKinds = allowedKinds.toSet(), disallowedKinds = disallowedKinds.toSet(), + roles = roleMap, + memberRoles = + roleAssignments + .associate { (pk, ids) -> pk.lowercase() to ids.filter { it in roleMap }.distinct() } + .filterValues { it.isNotEmpty() }, + claims = claims.toSet(), ), ) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/server/Nip86Server.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/server/Nip86Server.kt index 9e8ce452ee..1a838ea773 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/server/Nip86Server.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/server/Nip86Server.kt @@ -23,6 +23,8 @@ package com.vitorpamplona.quartz.nip86RelayManagement.server import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation +import com.vitorpamplona.quartz.nip43RelayMembers.roles.RelayRole +import com.vitorpamplona.quartz.nip86RelayManagement.rpc.AllowedEvent import com.vitorpamplona.quartz.nip86RelayManagement.rpc.AllowedPubkey import com.vitorpamplona.quartz.nip86RelayManagement.rpc.BannedEvent import com.vitorpamplona.quartz.nip86RelayManagement.rpc.BannedPubkey @@ -60,7 +62,16 @@ import kotlinx.serialization.json.int * * [supportedMethods] is the canonical list this server actually * implements; methods returned outside of it are no-ops and a NIP-86 - * client must not advertise them. + * client must not advertise them. Every admin on the [allowList] holds + * every permission, so the list is the same for every authorized caller + * (NIP-86 lets it be tailored per caller; there is nothing to tailor). + * + * The NIP-43 role (`createrole`, `editrole`, `deleterole`, `assignrole`, + * `unassignrole`) and invite-code (`listclaims`, `createclaim`, + * `deleteclaim`) methods only maintain the [BanStore]'s records. A relay + * that publishes kind 13534 / 33534 events or admits kind 28934 join + * requests reads them from there ([BanStore.listRoles], + * [BanStore.rolesOf], [BanStore.isValidClaim]). */ class Nip86Server( val banStore: BanStore, @@ -118,12 +129,24 @@ class Nip86Server( Nip86Method.ALLOW_PUBKEY, Nip86Method.UNALLOW_PUBKEY, Nip86Method.LIST_ALLOWED_PUBKEYS, + Nip86Method.CREATE_ROLE, + Nip86Method.EDIT_ROLE, + Nip86Method.DELETE_ROLE, + Nip86Method.ASSIGN_ROLE, + Nip86Method.UNASSIGN_ROLE, + Nip86Method.LIST_CLAIMS, + Nip86Method.CREATE_CLAIM, + Nip86Method.DELETE_CLAIM, Nip86Method.BAN_EVENT, + Nip86Method.UNBAN_EVENT, Nip86Method.ALLOW_EVENT, + Nip86Method.UNALLOW_EVENT, Nip86Method.LIST_BANNED_EVENTS, + Nip86Method.LIST_ALLOWED_EVENTS, Nip86Method.ALLOW_KIND, Nip86Method.DISALLOW_KIND, Nip86Method.LIST_ALLOWED_KINDS, + Nip86Method.LIST_DISALLOWED_KINDS, Nip86Method.CHANGE_RELAY_NAME, Nip86Method.CHANGE_RELAY_DESCRIPTION, Nip86Method.CHANGE_RELAY_ICON, @@ -190,14 +213,67 @@ class Nip86Server( } } + Nip86Method.UNBAN_EVENT -> { + withHex(req, "event_id") { id -> banStore.unbanEvent(id) } + } + Nip86Method.ALLOW_EVENT -> { - withHex(req, "event_id") { id -> banStore.allowEvent(id) } + withHexAndReason(req, "event_id") { id, reason -> banStore.allowEvent(id, reason) } + } + + Nip86Method.UNALLOW_EVENT -> { + withHex(req, "event_id") { id -> banStore.unallowEvent(id) } } Nip86Method.LIST_BANNED_EVENTS -> { ok(banStore.listBannedEvents().map { (id, r) -> BannedEvent(id, r) }.toJsonArray(BannedEvent.serializer())) } + Nip86Method.LIST_ALLOWED_EVENTS -> { + ok(banStore.listAllowedEvents().map { (id, r) -> AllowedEvent(id, r) }.toJsonArray(AllowedEvent.serializer())) + } + + Nip86Method.CREATE_ROLE -> { + withRole(req) { role -> + if (banStore.createRole(role)) okTrue else Nip86Response(error = "role already exists: ${role.id}") + } + } + + Nip86Method.EDIT_ROLE -> { + withRole(req) { role -> + if (banStore.editRole(role)) okTrue else Nip86Response(error = "unknown role: ${role.id}") + } + } + + Nip86Method.DELETE_ROLE -> { + withNonBlankString(req, "id") { id -> banStore.deleteRole(id) } + } + + Nip86Method.ASSIGN_ROLE -> { + withPubkeyAndRole(req) { pk, roleId -> + if (banStore.assignRole(pk, roleId)) okTrue else Nip86Response(error = "unknown role: $roleId") + } + } + + Nip86Method.UNASSIGN_ROLE -> { + withPubkeyAndRole(req) { pk, roleId -> + banStore.unassignRole(pk, roleId) + okTrue + } + } + + Nip86Method.LIST_CLAIMS -> { + ok(buildJsonArray { banStore.listClaims().forEach { add(JsonPrimitive(it)) } }) + } + + Nip86Method.CREATE_CLAIM -> { + withNonBlankString(req, "claim") { claim -> banStore.createClaim(claim) } + } + + Nip86Method.DELETE_CLAIM -> { + withNonBlankString(req, "claim") { claim -> banStore.deleteClaim(claim) } + } + Nip86Method.ALLOW_KIND -> { withInt(req, "kind") { k -> banStore.allowKind(k) } } @@ -214,6 +290,10 @@ class Nip86Server( ok(buildJsonArray { banStore.listAllowedKinds().forEach { add(JsonPrimitive(it)) } }) } + Nip86Method.LIST_DISALLOWED_KINDS -> { + ok(buildJsonArray { banStore.listDisallowedKinds().forEach { add(JsonPrimitive(it)) } }) + } + Nip86Method.CHANGE_RELAY_NAME -> { withString(req, "name") { name -> rewriteInfo { it.copy(name = name) } } } @@ -281,6 +361,45 @@ class Nip86Server( return okTrue } + private inline fun withNonBlankString( + req: Nip86Request, + label: String, + action: (String) -> Unit, + ): Nip86Response { + val v = req.params.firstString()?.takeIf { it.isNotBlank() } ?: return malformed("expected [$label]") + action(v) + return okTrue + } + + /** + * Parses NIP-86 `[id, label, description, color, order]`. Only `id` is + * required; trailing params may be omitted or `null`. `color` (a hue, + * 0..360) and `order` accept a JSON number or a numeric string, since + * the kind 33534 tags carry them as strings. + */ + private inline fun withRole( + req: Nip86Request, + action: (RelayRole) -> Nip86Response, + ): Nip86Response { + val id = req.params.firstString()?.takeIf { it.isNotBlank() } ?: return malformed("expected [id, label?, description?, color?, order?]") + val label = req.params.optString(1) ?: return malformed("label must be a string") + val description = req.params.optString(2) ?: return malformed("description must be a string") + val color = req.params.optInt(3) ?: return malformed("color must be an integer hue") + if (color.value != null && !RelayRole.isValidHue(color.value)) return malformed("color must be a hue between 0 and 360") + val order = req.params.optInt(4) ?: return malformed("order must be an integer") + return action(RelayRole(id, label.value?.ifEmpty { null }, description.value?.ifEmpty { null }, color.value, order.value)) + } + + private inline fun withPubkeyAndRole( + req: Nip86Request, + action: (HexKey, String) -> Nip86Response, + ): Nip86Response { + val (pk, roleId) = req.params.stringPair() ?: return malformed("expected [pubkey, role_id]") + if (!Hex.isHex64(pk)) return malformed("pubkey must be 64-char hex") + if (roleId.isNullOrBlank()) return malformed("expected [pubkey, role_id]") + return action(pk.lowercase(), roleId) + } + private fun rewriteInfo(transform: (Nip11RelayInformation) -> Nip11RelayInformation) { infoHolder.set(transform(infoHolder.get())) } @@ -304,6 +423,30 @@ private fun JsonArray.stringPair(): Pair<String, String?>? { private fun JsonArray.firstString(): String? = (getOrNull(0) as? JsonPrimitive)?.contentOrNull() +/** An optional positional param: [value] is null when the param is missing or JSON `null`. */ +private class OptionalParam<T>( + val value: T?, +) + +/** Missing / `null` -> empty; a JSON string -> its content; anything else -> null (malformed). */ +private fun JsonArray.optString(index: Int): OptionalParam<String>? { + val el = getOrNull(index) ?: return OptionalParam(null) + if (el == JsonNull) return OptionalParam(null) + val prim = el as? JsonPrimitive ?: return null + if (!prim.isString) return null + return OptionalParam(prim.content) +} + +/** Missing / `null` / "" -> empty; an integer number or numeric string -> its value; anything else -> null (malformed). */ +private fun JsonArray.optInt(index: Int): OptionalParam<Int>? { + val el = getOrNull(index) ?: return OptionalParam(null) + if (el == JsonNull) return OptionalParam(null) + val prim = el as? JsonPrimitive ?: return null + val text = prim.content.trim() + if (prim.isString && text.isEmpty()) return OptionalParam(null) + return text.toIntOrNull()?.let { OptionalParam(it) } +} + private fun JsonArray.firstInt(): Int? = runCatching { (this[0] as? JsonPrimitive)?.int diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt index 15d8d83388..2534fe4d28 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt @@ -250,6 +250,7 @@ import com.vitorpamplona.quartz.nip43RelayMembers.joinRequest.RelayJoinRequestEv import com.vitorpamplona.quartz.nip43RelayMembers.leaveRequest.RelayLeaveRequestEvent import com.vitorpamplona.quartz.nip43RelayMembers.list.RelayMembershipListEvent import com.vitorpamplona.quartz.nip43RelayMembers.removeMember.RelayRemoveMemberEvent +import com.vitorpamplona.quartz.nip43RelayMembers.roles.RelayRoleEvent import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent @@ -857,6 +858,7 @@ class EventFactory { RelayAddMemberEvent.KIND -> RelayAddMemberEvent(id, pubKey, createdAt, tags, content, sig) RelayRemoveMemberEvent.KIND -> RelayRemoveMemberEvent(id, pubKey, createdAt, tags, content, sig) RelayMembershipListEvent.KIND -> RelayMembershipListEvent(id, pubKey, createdAt, tags, content, sig) + RelayRoleEvent.KIND -> RelayRoleEvent(id, pubKey, createdAt, tags, content, sig) RelayJoinRequestEvent.KIND -> RelayJoinRequestEvent(id, pubKey, createdAt, tags, content, sig) RelayInviteRequestEvent.KIND -> RelayInviteRequestEvent(id, pubKey, createdAt, tags, content, sig) RelayLeaveRequestEvent.KIND -> RelayLeaveRequestEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/RelayRolesTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/RelayRolesTest.kt new file mode 100644 index 0000000000..95f097be5c --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip43RelayMembers/RelayRolesTest.kt @@ -0,0 +1,128 @@ +/* + * 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.quartz.nip43RelayMembers + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip43RelayMembers.inviteRequest.RelayInviteRequestEvent +import com.vitorpamplona.quartz.nip43RelayMembers.joinRequest.RelayJoinRequestEvent +import com.vitorpamplona.quartz.nip43RelayMembers.list.RelayMembershipListEvent +import com.vitorpamplona.quartz.nip43RelayMembers.list.tags.MemberTag +import com.vitorpamplona.quartz.nip43RelayMembers.list.tags.RelayMember +import com.vitorpamplona.quartz.nip43RelayMembers.roles.RelayRole +import com.vitorpamplona.quartz.nip43RelayMembers.roles.RelayRoleEvent +import com.vitorpamplona.quartz.nip70ProtectedEvts.isProtected +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertIs +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class RelayRolesTest { + private val signer = NostrSignerSync(KeyPair()) + private val alice = "c308e1f882c1f1dff2a43d4294239ddeec04e575f2d1aad1fa21ea7684e61fb5" + private val bob = "ee1d336e13779e4d4c527b988429d96de16088f958cbf6c074676ac9cfd9c958" + + @Test + fun roleEventBuildsTheSpecExampleAndParsesBack() { + val template = RelayRoleEvent.build(RelayRole("28b7e50f", "king", "ruler of the relay", 37, 1), createdAt = 1000) + assertEquals(RelayRoleEvent.KIND, template.kind) + assertContentEquals(arrayOf("-"), template.tags[0]) + assertContentEquals(arrayOf("d", "28b7e50f"), template.tags[1]) + assertContentEquals(arrayOf("label", "king"), template.tags[2]) + assertContentEquals(arrayOf("description", "ruler of the relay"), template.tags[3]) + assertContentEquals(arrayOf("color", "37"), template.tags[4]) + assertContentEquals(arrayOf("order", "1"), template.tags[5]) + + val signed = assertIs<RelayRoleEvent>(signer.sign(template)) + assertTrue(signed.isProtected()) + assertEquals("28b7e50f", signed.roleId()) + assertEquals(RelayRole("28b7e50f", "king", "ruler of the relay", 37, 1), signed.role()) + } + + @Test + fun roleEventOptionalTagsAndBadValues() { + val minimal = assertIs<RelayRoleEvent>(signer.sign(RelayRoleEvent.build(RelayRole("r1")))) + assertEquals(RelayRole("r1"), minimal.role()) + + // A foreign event with an out-of-range hue and a non-numeric order. + val odd = + assertIs<RelayRoleEvent>( + signer.sign<Event>( + 1000, + RelayRoleEvent.KIND, + arrayOf(arrayOf("-"), arrayOf("d", "r2"), arrayOf("color", "400"), arrayOf("order", "first")), + "", + ), + ) + assertNull(odd.color()) + assertNull(odd.order()) + + assertFailsWith<IllegalArgumentException> { RelayRoleEvent.build(RelayRole("r3", color = 361)) } + } + + @Test + fun memberTagCarriesOptionalRoles() { + val withRoles = arrayOf("member", bob, "28b7e50f", "", "abc", "28b7e50f") + assertEquals(bob, MemberTag.parse(withRoles)) + assertEquals(RelayMember(bob, listOf("28b7e50f", "abc")), MemberTag.parseMember(withRoles)) + assertEquals(RelayMember(alice), MemberTag.parseMember(arrayOf("member", alice))) + assertNull(MemberTag.parseMember(arrayOf("member", "short"))) + + assertContentEquals(arrayOf("member", bob, "r1", "r2"), MemberTag.assemble(bob, listOf("r1", "r2"))) + assertContentEquals(arrayOf("member", alice), MemberTag.assemble(RelayMember(alice))) + } + + @Test + fun membershipListWithRolesKeepsPlainMembersBackwardCompatible() { + val template = + RelayMembershipListEvent.buildWithRoles( + listOf(RelayMember(alice), RelayMember(bob, listOf("28b7e50f"))), + ) + val signed = assertIs<RelayMembershipListEvent>(signer.sign(template)) + assertTrue(signed.isProtected()) + assertEquals(listOf(alice, bob), signed.members()) + assertEquals(listOf(RelayMember(alice), RelayMember(bob, listOf("28b7e50f"))), signed.membersWithRoles()) + + val legacy = assertIs<RelayMembershipListEvent>(signer.sign(RelayMembershipListEvent.build(listOf(alice)))) + assertEquals(listOf(RelayMember(alice)), legacy.membersWithRoles()) + } + + @Test + fun joinRequestCarriesClaimAndProtectedTag() { + val signed = assertIs<RelayJoinRequestEvent>(signer.sign(RelayJoinRequestEvent.build("invite-code"))) + assertTrue(signed.isProtected()) + assertEquals("invite-code", signed.claim()) + + assertFailsWith<IllegalArgumentException> { RelayJoinRequestEvent.build(" ") } + } + + @Suppress("DEPRECATION") + @Test + fun deprecatedInviteRequestStillParses() { + val signed = + signer.sign<Event>(1000, RelayInviteRequestEvent.KIND, arrayOf(arrayOf("-"), arrayOf("claim", "abc")), "") + assertIs<RelayInviteRequestEvent>(signed) + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/Nip86RequestResponseTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/Nip86RequestResponseTest.kt new file mode 100644 index 0000000000..32b5168a99 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/Nip86RequestResponseTest.kt @@ -0,0 +1,78 @@ +/* + * 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.quartz.nip86RelayManagement + +import com.vitorpamplona.quartz.nip01Core.core.JsonMapper +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal +import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Method +import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Request +import kotlin.test.Test +import kotlin.test.assertEquals + +class Nip86RequestResponseTest { + private val client = Nip86Client(RelayUrlNormalizer.normalize("wss://relay.example.com"), NostrSignerInternal(KeyPair())) + private val id = "c".repeat(64) + private val pk = "a".repeat(64) + + private fun json(req: Nip86Request) = client.serializeRequest(req) + + @Test + fun eventAllowAndBanMethods() { + assertEquals("""{"method":"allowevent","params":["$id","ok"]}""", json(Nip86Request.allowEvent(id, "ok"))) + assertEquals("""{"method":"unallowevent","params":["$id"]}""", json(Nip86Request.unallowEvent(id))) + assertEquals("""{"method":"unbanevent","params":["$id","oops"]}""", json(Nip86Request.unbanEvent(id, "oops"))) + assertEquals("""{"method":"listallowedevents","params":[]}""", json(Nip86Request.listAllowedEvents())) + assertEquals("""{"method":"listdisallowedkinds","params":[]}""", json(Nip86Request.listDisallowedKinds())) + } + + @Test + fun roleMethods() { + assertEquals( + """{"method":"createrole","params":["28b7e50f","king","ruler of the relay",37,1]}""", + json(Nip86Request.createRole("28b7e50f", "king", "ruler of the relay", 37, 1)), + ) + assertEquals("""{"method":"editrole","params":["r",null,null,null,null]}""", json(Nip86Request.editRole("r"))) + assertEquals("""{"method":"deleterole","params":["r"]}""", json(Nip86Request.deleteRole("r"))) + assertEquals("""{"method":"assignrole","params":["$pk","r"]}""", json(Nip86Request.assignRole(pk, "r"))) + assertEquals("""{"method":"unassignrole","params":["$pk","r"]}""", json(Nip86Request.unassignRole(pk, "r"))) + } + + @Test + fun claimMethods() { + assertEquals("""{"method":"listclaims","params":[]}""", json(Nip86Request.listClaims())) + assertEquals("""{"method":"createclaim","params":["abc"]}""", json(Nip86Request.createClaim("abc"))) + assertEquals("""{"method":"deleteclaim","params":["abc"]}""", json(Nip86Request.deleteClaim("abc"))) + assertEquals(Nip86Method.CREATE_CLAIM, JsonMapper.fromJson<Nip86Request>(json(Nip86Request.createClaim("abc"))).method) + } + + @Test + fun parsesNewResponses() { + val allowed = client.parseAllowedEvents(client.parseResponse("""{"result":[{"id":"$id","reason":"ok"},{"id":"$id"}]}"""))!! + assertEquals(listOf(id, id), allowed.map { it.id }) + assertEquals(listOf("ok", null), allowed.map { it.reason }) + + assertEquals(listOf(4, 1059), client.parseDisallowedKinds(client.parseResponse("""{"result":[4,1059]}"""))) + assertEquals(listOf("a", "b"), client.parseClaims(client.parseResponse("""{"result":["a","b"]}"""))) + assertEquals(true, client.parseBooleanResult(client.parseResponse("""{"result":true}"""))) + } +} diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/server/BanStoreTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/server/BanStoreTest.kt index 66bfdb0f29..3a1058f56f 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/server/BanStoreTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/server/BanStoreTest.kt @@ -20,6 +20,10 @@ */ package com.vitorpamplona.quartz.nip86RelayManagement.server +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd +import com.vitorpamplona.quartz.nip01Core.relay.server.policies.PolicyResult +import com.vitorpamplona.quartz.nip43RelayMembers.roles.RelayRole import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -60,8 +64,151 @@ class BanStoreTest { val s = BanStore() s.banEvent("ee".padEnd(64, '0'), "policy") assertTrue(s.isBannedEvent("EE".padEnd(64, '0'))) - s.allowEvent("ee".padEnd(64, '0')) + s.unbanEvent("ee".padEnd(64, '0')) assertFalse(s.isBannedEvent("ee".padEnd(64, '0'))) + assertFalse(s.isAllowedEvent("ee".padEnd(64, '0')), "unban must not allow-list") + } + + @Test + fun banAndAllowAreMutuallyExclusiveForPubkeys() { + val s = BanStore() + val pk = "aa".padEnd(64, '0') + s.allowPubkey(pk, "trusted") + s.banPubkey(pk, "spam") + assertTrue(s.isBanned(pk)) + assertFalse(s.isAllowedPubkey(pk), "banpubkey must remove from the allow list") + + s.allowPubkey(pk.uppercase(), "trusted again") + assertTrue(s.isAllowedPubkey(pk)) + assertFalse(s.isBanned(pk), "allowpubkey must remove from the ban list") + + s.unallowPubkey(pk) + assertFalse(s.isAllowedPubkey(pk)) + assertFalse(s.isBanned(pk), "unallowpubkey must not ban") + + s.banPubkey(pk) + s.unbanPubkey(pk) + assertFalse(s.isBanned(pk)) + assertFalse(s.isAllowedPubkey(pk), "unbanpubkey must not allow-list") + } + + @Test + fun banAndAllowAreMutuallyExclusiveForEvents() { + val s = BanStore() + val id = "ee".padEnd(64, '0') + s.allowEvent(id, "approved") + assertTrue(s.isAllowedEvent(id)) + assertEquals(listOf(id to "approved"), s.listAllowedEvents()) + + s.banEvent(id, "spam") + assertTrue(s.isBannedEvent(id)) + assertFalse(s.isAllowedEvent(id), "banevent must remove from the allow list") + + s.allowEvent(id) + assertTrue(s.isAllowedEvent(id)) + assertFalse(s.isBannedEvent(id), "allowevent must remove from the ban list") + + s.unallowEvent(id) + assertFalse(s.isAllowedEvent(id)) + assertFalse(s.isBannedEvent(id), "unallowevent must not ban") + } + + @Test + fun rolesAssignmentsAndDeletion() { + val s = BanStore() + val pk = "aa".padEnd(64, '0') + assertTrue(s.createRole(RelayRole("b", label = "B", order = 2))) + assertTrue(s.createRole(RelayRole("a", label = "A", order = 1))) + assertTrue(s.createRole(RelayRole("z"))) + assertFalse(s.createRole(RelayRole("a", label = "dup"))) + assertEquals(listOf("a", "b", "z"), s.listRoles().map { it.id }, "sorted by order, unordered last") + + assertFalse(s.editRole(RelayRole("missing"))) + assertTrue(s.editRole(RelayRole("a", label = "Alpha", color = 30))) + assertEquals(RelayRole("a", label = "Alpha", color = 30), s.getRole("a")) + + assertFalse(s.assignRole(pk, "missing")) + assertTrue(s.assignRole(pk.uppercase(), "a")) + assertTrue(s.assignRole(pk, "b")) + assertTrue(s.assignRole(pk, "a")) + assertEquals(listOf("a", "b"), s.rolesOf(pk)) + + s.deleteRole("a") + assertEquals(listOf("b"), s.rolesOf(pk)) + s.unassignRole(pk, "b") + assertEquals(emptyList(), s.rolesOf(pk)) + assertEquals(emptyList(), s.listRoleAssignments()) + } + + @Test + fun claims() { + val s = BanStore() + s.createClaim("code-1") + s.createClaim("code-1") + s.createClaim("code-2") + assertEquals(listOf("code-1", "code-2"), s.listClaims()) + assertTrue(s.isValidClaim("code-2")) + s.deleteClaim("code-2") + assertFalse(s.isValidClaim("code-2")) + } + + @Test + fun mutationsFireHookButFailedRoleEditsDoNot() { + var count = 0 + val s = BanStore(onMutation = { count++ }) + s.allowEvent("ee".padEnd(64, '0')) + assertEquals(1, count) + s.editRole(RelayRole("missing")) + s.assignRole("aa".padEnd(64, '0'), "missing") + assertEquals(1, count) + } + + @Test + fun seedFromSnapshotRestoresNewSectionsAndResolvesConflicts() { + val s = BanStore() + val pk = "aa".padEnd(64, '0') + val id = "ee".padEnd(64, '0') + s.seedFromSnapshot( + bannedPubkeys = listOf(pk to "spam"), + allowedPubkeys = listOf(pk to "hand-edited conflict"), + bannedEvents = listOf(id to "x"), + allowedEvents = listOf(id to "conflict", "ff".padEnd(64, '0') to "ok"), + roles = listOf(RelayRole("mod")), + roleAssignments = listOf(pk to listOf("mod", "ghost")), + claims = listOf("c"), + ) + assertTrue(s.isBanned(pk)) + assertFalse(s.isAllowedPubkey(pk)) + assertTrue(s.isBannedEvent(id)) + assertFalse(s.isAllowedEvent(id)) + assertTrue(s.isAllowedEvent("ff".padEnd(64, '0'))) + assertEquals(listOf("mod"), s.rolesOf(pk), "assignments to unknown roles are dropped") + assertTrue(s.isValidClaim("c")) + } + + @Test + fun policyLetsAllowListedEventsBypassPubkeyAndKindRules() { + val s = BanStore() + val policy = BanListPolicy(s) + val author = "46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d" + val allowedId = "a".repeat(64) + val otherId = "b".repeat(64) + + fun event(id: String) = Event(id, author, 1000L, 1, emptyArray(), "hi", "0".repeat(128)) + + // Empty event allow list changes nothing. + assertTrue(policy.accept(EventCmd(event(otherId))) is PolicyResult.Accepted) + + s.banPubkey(author) + s.disallowKind(1) + assertTrue(policy.accept(EventCmd(event(otherId))) is PolicyResult.Rejected) + + s.allowEvent(allowedId) + assertTrue(policy.accept(EventCmd(event(allowedId))) is PolicyResult.Accepted) + assertTrue(policy.accept(EventCmd(event(otherId))) is PolicyResult.Rejected) + + s.banEvent(allowedId) + assertTrue(policy.accept(EventCmd(event(allowedId))) is PolicyResult.Rejected) } @Test diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/server/Nip86ServerTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/server/Nip86ServerTest.kt index 534eb84b4e..615009c503 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/server/Nip86ServerTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip86RelayManagement/server/Nip86ServerTest.kt @@ -21,19 +21,25 @@ package com.vitorpamplona.quartz.nip86RelayManagement.server import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation +import com.vitorpamplona.quartz.nip43RelayMembers.roles.RelayRole +import com.vitorpamplona.quartz.nip86RelayManagement.rpc.AllowedEvent import com.vitorpamplona.quartz.nip86RelayManagement.rpc.AllowedPubkey import com.vitorpamplona.quartz.nip86RelayManagement.rpc.BannedEvent import com.vitorpamplona.quartz.nip86RelayManagement.rpc.BannedPubkey import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Method import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Request import kotlinx.coroutines.runBlocking +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonPrimitive import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -140,9 +146,167 @@ class Nip86ServerTest { assertEquals(eventId, list[0].id) assertEquals("off-topic", list[0].reason) - // allowevent (which is "unban") removes the entry. - server.dispatch(admin, Nip86Request.allowEvent(eventId)) + // unbanevent removes the entry without allow-listing it. + server.dispatch(admin, Nip86Request.unbanEvent(eventId)) assertTrue(banStore.listBannedEvents().isEmpty()) + assertTrue(banStore.listAllowedEvents().isEmpty()) + } + } + + @Test + fun allowEventAddsToAllowListWithReasonAndLiftsBan() { + runBlocking { + val (server, banStore, _) = fixture() + server.dispatch(admin, Nip86Request.banEvent(eventId, "spam")) + + val ok = server.dispatch(admin, Nip86Request.allowEvent(eventId, "reviewed")) + assertEquals(true, (ok.result as JsonPrimitive).boolean) + assertTrue(banStore.isAllowedEvent(eventId)) + assertFalse(banStore.isBannedEvent(eventId)) + + val resp = server.dispatch(admin, Nip86Request.listAllowedEvents()) + val list = Json.decodeFromJsonElement(ListSerializer(AllowedEvent.serializer()), resp.result as JsonArray) + assertEquals(1, list.size) + assertEquals(eventId, list[0].id) + assertEquals("reviewed", list[0].reason) + + // banevent moves it back off the allow list. + server.dispatch(admin, Nip86Request.banEvent(eventId, "again")) + assertTrue(banStore.isBannedEvent(eventId)) + assertFalse(banStore.isAllowedEvent(eventId)) + + // unallowevent / unbanevent never add to the opposite list. + server.dispatch(admin, Nip86Request.allowEvent(eventId)) + server.dispatch(admin, Nip86Request.unallowEvent(eventId)) + assertFalse(banStore.isAllowedEvent(eventId)) + assertFalse(banStore.isBannedEvent(eventId)) + } + } + + @Test + fun banAndAllowPubkeyAreMutuallyExclusive() { + runBlocking { + val (server, banStore, _) = fixture() + server.dispatch(admin, Nip86Request.allowPubkey(pk, "trusted")) + server.dispatch(admin, Nip86Request.banPubkey(pk, "spam")) + assertTrue(banStore.isBanned(pk)) + assertFalse(banStore.isAllowedPubkey(pk)) + + server.dispatch(admin, Nip86Request.allowPubkey(pk)) + assertTrue(banStore.isAllowedPubkey(pk)) + assertFalse(banStore.isBanned(pk)) + + server.dispatch(admin, Nip86Request.unallowPubkey(pk)) + assertFalse(banStore.isAllowedPubkey(pk)) + assertFalse(banStore.isBanned(pk)) + } + } + + @Test + fun listDisallowedKinds() { + runBlocking { + val (server, _, _) = fixture() + server.dispatch(admin, Nip86Request.disallowKind(4)) + server.dispatch(admin, Nip86Request.disallowKind(1059)) + val resp = server.dispatch(admin, Nip86Request.listDisallowedKinds()) + assertEquals(listOf(4, 1059), (resp.result as JsonArray).map { it.jsonPrimitive.int }) + } + } + + @Test + fun roleLifecycle() { + runBlocking { + val (server, banStore, _) = fixture() + val created = server.dispatch(admin, Nip86Request.createRole("mod", "Moderator", "keeps order", 120, 1)) + assertNull(created.error) + assertEquals(RelayRole("mod", "Moderator", "keeps order", 120, 1), banStore.getRole("mod")) + + // Creating an existing id is refused; edit replaces it. + assertNotNull(server.dispatch(admin, Nip86Request.createRole("mod")).error) + assertNull(server.dispatch(admin, Nip86Request.editRole("mod", "Mods", null, 200, null)).error) + assertEquals(RelayRole("mod", "Mods", null, 200, null), banStore.getRole("mod")) + assertNotNull(server.dispatch(admin, Nip86Request.editRole("nope", "x")).error) + + assertNull(server.dispatch(admin, Nip86Request.assignRole(pk, "mod")).error) + assertEquals(listOf("mod"), banStore.rolesOf(pk)) + assertNotNull(server.dispatch(admin, Nip86Request.assignRole(pk, "nope")).error) + + assertNull(server.dispatch(admin, Nip86Request.unassignRole(pk, "mod")).error) + assertEquals(emptyList(), banStore.rolesOf(pk)) + + server.dispatch(admin, Nip86Request.assignRole(pk2, "mod")) + assertNull(server.dispatch(admin, Nip86Request.deleteRole("mod")).error) + assertNull(banStore.getRole("mod")) + assertEquals(emptyList(), banStore.rolesOf(pk2), "deleting a role unassigns it") + } + } + + @Test + fun roleParamsAcceptStringNumbersAndRejectBadHues() { + runBlocking { + val (server, banStore, _) = fixture() + val stringy = + Nip86Request( + method = Nip86Method.CREATE_ROLE, + params = + buildJsonArray { + add(JsonPrimitive("king")) + add(JsonPrimitive("king")) + add(JsonPrimitive("ruler of the relay")) + add(JsonPrimitive("37")) + add(JsonPrimitive("1")) + }, + ) + assertNull(server.dispatch(admin, stringy).error) + assertEquals(RelayRole("king", "king", "ruler of the relay", 37, 1), banStore.getRole("king")) + + // Only the id is required. + val idOnly = Nip86Request(method = Nip86Method.CREATE_ROLE, params = buildJsonArray { add(JsonPrimitive("bare")) }) + assertNull(server.dispatch(admin, idOnly).error) + assertEquals(RelayRole("bare"), banStore.getRole("bare")) + + assertNotNull(server.dispatch(admin, Nip86Request.createRole("hot", color = 361)).error) + assertNull(banStore.getRole("hot")) + assertNotNull(server.dispatch(admin, Nip86Request(method = Nip86Method.CREATE_ROLE)).error) + } + } + + @Test + fun claimLifecycle() { + runBlocking { + val (server, banStore, _) = fixture() + assertNull(server.dispatch(admin, Nip86Request.createClaim("invite-1")).error) + assertNull(server.dispatch(admin, Nip86Request.createClaim("invite-2")).error) + assertTrue(banStore.isValidClaim("invite-1")) + + val listed = server.dispatch(admin, Nip86Request.listClaims()) + assertEquals(setOf("invite-1", "invite-2"), (listed.result as JsonArray).map { it.jsonPrimitive.content }.toSet()) + + assertNull(server.dispatch(admin, Nip86Request.deleteClaim("invite-1")).error) + assertFalse(banStore.isValidClaim("invite-1")) + assertNotNull(server.dispatch(admin, Nip86Request.createClaim(" ")).error) + } + } + + @Test + fun supportedMethodsAreAllDispatchable() { + runBlocking { + val (server, _, _) = fixture() + // Every advertised method must reach a real handler, never "method not supported". + server.supportedMethods.forEach { method -> + val resp = server.dispatch(admin, Nip86Request(method = method)) + assertFalse(resp.error?.startsWith("method not supported") == true, "advertised but not handled: " + method) + } + listOf( + Nip86Method.UNBAN_EVENT, + Nip86Method.UNALLOW_EVENT, + Nip86Method.LIST_ALLOWED_EVENTS, + Nip86Method.LIST_DISALLOWED_KINDS, + Nip86Method.CREATE_ROLE, + Nip86Method.ASSIGN_ROLE, + Nip86Method.LIST_CLAIMS, + Nip86Method.CREATE_CLAIM, + ).forEach { assertTrue(it in server.supportedMethods, it) } } } From 7ed0190fc3aca56307d760a576c0426ab2e4ee1f Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Sun, 27 Sep 2026 17:05:19 +0000 Subject: [PATCH 60/60] feat: NIP-86 roles/claims/event allow list in amy and relay management UI amy admin gains unban-event, unallow-event, list-allowed-events, list-disallowed-kinds, create/edit/delete-role, assign/unassign-role and list/create/delete-claim; allow-event is documented as "allow-list". Android relay management: the moderation queue's approve button keeps using allowevent (which now allow-lists and lifts any ban) and refreshes every event list; banned events can be unbanned, allowed events are listed with unallow, and disallowed kinds are shown read-only. NIP-43 members screen: filters 13534/33534 by the relay's NIP-11 self, shows each member's roles as hue-tinted chips, and asks for the invite code the join request (28934) now requires. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MGR1u8SyzcUuekub39SBsc --- .../relays/nip43/RelayMembersScreen.kt | 108 ++++++++++++++---- .../relays/nip86/RelayManagementScreen.kt | 88 +++++++++++++- .../relays/nip86/RelayManagementViewModel.kt | 92 +++++++++++++-- cli/README.md | 8 +- cli/ROADMAP.md | 2 +- .../amethyst/cli/commands/AdminCommand.kt | 60 +++++++++- .../composeResources/values/strings.xml | 7 ++ .../nip43RelayMembers/ui/RelayMemberCards.kt | 66 +++++++++++ 8 files changed, 389 insertions(+), 42 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip43/RelayMembersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip43/RelayMembersScreen.kt index fad68faca1..c33f421c56 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip43/RelayMembersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip43/RelayMembersScreen.kt @@ -41,6 +41,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar @@ -59,9 +60,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.nip43RelayMembers.ui.RelayRoleChips import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.relay_members_count import com.vitorpamplona.amethyst.commons.resources.relay_members_empty +import com.vitorpamplona.amethyst.commons.resources.relay_members_invite_code +import com.vitorpamplona.amethyst.commons.resources.relay_members_invite_code_hint import com.vitorpamplona.amethyst.commons.resources.relay_members_join_sent import com.vitorpamplona.amethyst.commons.resources.relay_members_leave_sent import com.vitorpamplona.amethyst.commons.resources.relay_members_loading @@ -72,9 +76,9 @@ import com.vitorpamplona.amethyst.commons.resources.relay_members_you_are_member import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn +import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.fetchAsFlow import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -83,6 +87,9 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip43RelayMembers.joinRequest.RelayJoinRequestEvent import com.vitorpamplona.quartz.nip43RelayMembers.leaveRequest.RelayLeaveRequestEvent import com.vitorpamplona.quartz.nip43RelayMembers.list.RelayMembershipListEvent +import com.vitorpamplona.quartz.nip43RelayMembers.list.tags.RelayMember +import com.vitorpamplona.quartz.nip43RelayMembers.roles.RelayRole +import com.vitorpamplona.quartz.nip43RelayMembers.roles.RelayRoleEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.lastOrNull import kotlinx.coroutines.launch @@ -97,24 +104,32 @@ fun RelayMembersScreen( val normalizedRelayUrl = remember(relayUrl) { RelayUrlNormalizer.normalizeOrNull(relayUrl) } if (normalizedRelayUrl == null) return - var members by remember { mutableStateOf<List<HexKey>>(emptyList()) } + var members by remember { mutableStateOf<List<RelayMember>>(emptyList()) } + var roles by remember { mutableStateOf<Map<String, RelayRole>>(emptyMap()) } var isLoading by remember { mutableStateOf(true) } var isMember by remember { mutableStateOf(false) } var joinRequestSent by remember { mutableStateOf(false) } var leaveRequestSent by remember { mutableStateOf(false) } + var inviteCode by remember { mutableStateOf("") } val scope = rememberCoroutineScope() - LaunchedEffect(normalizedRelayUrl) { + // NIP-43 lists (13534) and roles (33534) MUST be signed by the relay's NIP-11 `self`. + // Filter by it once the doc resolves; until then, take whatever the relay serves. + val relayInfo by loadRelayInfo(normalizedRelayUrl) + val relaySelf = relayInfo.self + + LaunchedEffect(normalizedRelayUrl, relaySelf) { launch(Dispatchers.IO) { - val filter = - Filter( - kinds = listOf(RelayMembershipListEvent.KIND), - limit = 1, + val authors = relaySelf?.let { listOf(it) } + val filters = + listOf( + Filter(kinds = listOf(RelayMembershipListEvent.KIND), authors = authors, limit = 1), + Filter(kinds = listOf(RelayRoleEvent.KIND), authors = authors), ) val events = accountViewModel.account.client - .fetchAsFlow(normalizedRelayUrl, filter) + .fetchAsFlow(normalizedRelayUrl, filters) .lastOrNull() val membershipEvent = @@ -122,9 +137,19 @@ fun RelayMembersScreen( ?.mapNotNull { it as? RelayMembershipListEvent } ?.maxByOrNull { it.createdAt } - val memberList = membershipEvent?.members() ?: emptyList() + // Only trust role definitions from whoever signed the member list. + val roleSigner = relaySelf ?: membershipEvent?.pubKey + roles = + events + ?.mapNotNull { it as? RelayRoleEvent } + ?.filter { it.pubKey == roleSigner } + ?.groupBy { it.roleId() } + ?.mapValues { (_, versions) -> versions.maxBy { it.createdAt }.role() } + ?: emptyMap() + + val memberList = membershipEvent?.membersWithRoles() ?: emptyList() members = memberList - isMember = memberList.contains(accountViewModel.account.signer.pubKey) + isMember = memberList.any { it.pubKey == accountViewModel.account.signer.pubKey } isLoading = false } } @@ -162,9 +187,12 @@ fun RelayMembersScreen( isLoading = isLoading, joinRequestSent = joinRequestSent, leaveRequestSent = leaveRequestSent, + inviteCode = inviteCode, + onInviteCodeChange = { inviteCode = it }, onJoinRequest = { + val claim = inviteCode.trim() accountViewModel.launchSigner { - sendJoinRequest(normalizedRelayUrl, accountViewModel) + sendJoinRequest(normalizedRelayUrl, claim, accountViewModel) joinRequestSent = true } }, @@ -215,13 +243,21 @@ fun RelayMembersScreen( ) LazyColumn(modifier = Modifier.fillMaxSize()) { - items(members, key = { it }) { memberPubKey -> - val user = remember(memberPubKey) { accountViewModel.account.cache.getOrCreateUser(memberPubKey) } - UserCompose( - baseUser = user, - accountViewModel = accountViewModel, - nav = nav, - ) + items(members, key = { it.pubKey }) { member -> + val user = remember(member.pubKey) { accountViewModel.account.cache.getOrCreateUser(member.pubKey) } + Column { + UserCompose( + baseUser = user, + accountViewModel = accountViewModel, + nav = nav, + ) + // Role ids without a published 33534 definition still show, by id. + val memberRoles = remember(member, roles) { member.roles.map { roles[it] ?: RelayRole(it) } } + RelayRoleChips( + roles = memberRoles, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp), + ) + } } } } @@ -235,9 +271,32 @@ fun MembershipActions( isLoading: Boolean, joinRequestSent: Boolean, leaveRequestSent: Boolean, + inviteCode: String, + onInviteCodeChange: (String) -> Unit, onJoinRequest: () -> Unit, onLeaveRequest: () -> Unit, ) { + if (!isLoading && !isMember && !joinRequestSent) { + // NIP-43 join requests (kind 28934) must carry the invite code the relay issued. + Column( + modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, top = 16.dp), + ) { + Text( + text = stringRes(Res.string.relay_members_invite_code_hint), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = inviteCode, + onValueChange = onInviteCodeChange, + label = { Text(stringRes(Res.string.relay_members_invite_code)) }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) + } + } + Row( modifier = Modifier @@ -293,7 +352,7 @@ fun MembershipActions( fontWeight = FontWeight.Bold, ) } else { - Button(onClick = onJoinRequest) { + Button(onClick = onJoinRequest, enabled = inviteCode.isNotBlank()) { Icon( symbol = MaterialSymbols.PersonAdd, contentDescription = null, @@ -316,6 +375,8 @@ private fun MembershipActionsNotMemberPreview() { isLoading = false, joinRequestSent = false, leaveRequestSent = false, + inviteCode = "", + onInviteCodeChange = {}, onJoinRequest = {}, onLeaveRequest = {}, ) @@ -331,6 +392,8 @@ private fun MembershipActionsIsMemberPreview() { isLoading = false, joinRequestSent = false, leaveRequestSent = false, + inviteCode = "", + onInviteCodeChange = {}, onJoinRequest = {}, onLeaveRequest = {}, ) @@ -346,6 +409,8 @@ private fun MembershipActionsJoinSentPreview() { isLoading = false, joinRequestSent = true, leaveRequestSent = false, + inviteCode = "", + onInviteCodeChange = {}, onJoinRequest = {}, onLeaveRequest = {}, ) @@ -361,6 +426,8 @@ private fun MembershipActionsLeaveSentPreview() { isLoading = false, joinRequestSent = false, leaveRequestSent = true, + inviteCode = "", + onInviteCodeChange = {}, onJoinRequest = {}, onLeaveRequest = {}, ) @@ -369,9 +436,10 @@ private fun MembershipActionsLeaveSentPreview() { suspend fun sendJoinRequest( relay: NormalizedRelayUrl, + claim: String, accountViewModel: AccountViewModel, ) { - val template = RelayJoinRequestEvent.build() + val template = RelayJoinRequestEvent.build(claim) val signedEvent = accountViewModel.account.signer.sign(template) accountViewModel.account.cache.justConsumeMyOwnEvent(signedEvent) accountViewModel.account.client.publish(signedEvent, setOf(relay)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt index fd0e1f5a7c..5f404dc434 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt @@ -82,8 +82,10 @@ import com.vitorpamplona.amethyst.commons.relayManagement.Nip86Retriever import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.relay_management_add import com.vitorpamplona.amethyst.commons.resources.relay_management_allow +import com.vitorpamplona.amethyst.commons.resources.relay_management_allow_event import com.vitorpamplona.amethyst.commons.resources.relay_management_allow_kind import com.vitorpamplona.amethyst.commons.resources.relay_management_allow_pubkey +import com.vitorpamplona.amethyst.commons.resources.relay_management_allowed_events import com.vitorpamplona.amethyst.commons.resources.relay_management_allowed_kinds import com.vitorpamplona.amethyst.commons.resources.relay_management_allowed_pubkeys import com.vitorpamplona.amethyst.commons.resources.relay_management_apply @@ -96,6 +98,7 @@ import com.vitorpamplona.amethyst.commons.resources.relay_management_block_ip import com.vitorpamplona.amethyst.commons.resources.relay_management_blocked_ips import com.vitorpamplona.amethyst.commons.resources.relay_management_cancel import com.vitorpamplona.amethyst.commons.resources.relay_management_confirm +import com.vitorpamplona.amethyst.commons.resources.relay_management_disallowed_kinds import com.vitorpamplona.amethyst.commons.resources.relay_management_dismiss import com.vitorpamplona.amethyst.commons.resources.relay_management_error import com.vitorpamplona.amethyst.commons.resources.relay_management_event_id_hex @@ -103,11 +106,13 @@ import com.vitorpamplona.amethyst.commons.resources.relay_management_ip_address import com.vitorpamplona.amethyst.commons.resources.relay_management_kind_number import com.vitorpamplona.amethyst.commons.resources.relay_management_loading import com.vitorpamplona.amethyst.commons.resources.relay_management_moderation_queue +import com.vitorpamplona.amethyst.commons.resources.relay_management_no_allowed_events import com.vitorpamplona.amethyst.commons.resources.relay_management_no_allowed_kinds import com.vitorpamplona.amethyst.commons.resources.relay_management_no_allowed_pubkeys import com.vitorpamplona.amethyst.commons.resources.relay_management_no_banned_events import com.vitorpamplona.amethyst.commons.resources.relay_management_no_banned_pubkeys import com.vitorpamplona.amethyst.commons.resources.relay_management_no_blocked_ips +import com.vitorpamplona.amethyst.commons.resources.relay_management_no_disallowed_kinds import com.vitorpamplona.amethyst.commons.resources.relay_management_no_methods import com.vitorpamplona.amethyst.commons.resources.relay_management_no_moderation_events import com.vitorpamplona.amethyst.commons.resources.relay_management_reason_optional @@ -288,12 +293,19 @@ private fun RelayManagementContent( add(ManagementTab.PUBKEYS) } if (supportedMethods.any { - it in listOf(Nip86Method.BAN_EVENT, Nip86Method.LIST_BANNED_EVENTS, Nip86Method.ALLOW_EVENT, Nip86Method.LIST_EVENTS_NEEDING_MODERATION) + it in + listOf( + Nip86Method.BAN_EVENT, + Nip86Method.LIST_BANNED_EVENTS, + Nip86Method.ALLOW_EVENT, + Nip86Method.LIST_ALLOWED_EVENTS, + Nip86Method.LIST_EVENTS_NEEDING_MODERATION, + ) } ) { add(ManagementTab.EVENTS) } - if (supportedMethods.any { it in listOf(Nip86Method.ALLOW_KIND, Nip86Method.DISALLOW_KIND, Nip86Method.LIST_ALLOWED_KINDS) }) { + if (supportedMethods.any { it in listOf(Nip86Method.ALLOW_KIND, Nip86Method.DISALLOW_KIND, Nip86Method.LIST_ALLOWED_KINDS, Nip86Method.LIST_DISALLOWED_KINDS) }) { add(ManagementTab.KINDS) } if (supportedMethods.any { it in listOf(Nip86Method.BLOCK_IP, Nip86Method.UNBLOCK_IP, Nip86Method.LIST_BLOCKED_IPS) }) { @@ -571,8 +583,10 @@ private fun EventsTab( supportedMethods: List<String>, ) { val bannedEvents by viewModel.bannedEvents.collectAsState() + val allowedEvents by viewModel.allowedEvents.collectAsState() val eventsNeedingModeration by viewModel.eventsNeedingModeration.collectAsState() var showBanDialog by remember { mutableStateOf(false) } + var showAllowDialog by remember { mutableStateOf(false) } LazyColumn( contentPadding = PaddingValues(10.dp), @@ -596,6 +610,7 @@ private fun EventsTab( reason = entry.reason, canAllow = supportedMethods.contains(Nip86Method.ALLOW_EVENT), canBan = supportedMethods.contains(Nip86Method.BAN_EVENT), + // Approve: `allowevent` allow-lists the event (and lifts any ban). onAllow = { viewModel.allowEvent(entry.id) }, onBan = { viewModel.banEvent(entry.id) }, ) @@ -616,16 +631,52 @@ private fun EventsTab( if (bannedEvents.isEmpty()) { item { EmptyListMessage(stringRes(Res.string.relay_management_no_banned_events)) } } else { - items(bannedEvents, key = { it.id }) { entry -> + items(bannedEvents, key = { "banned" + it.id }) { entry -> HexEntryCard( hex = entry.id, reason = entry.reason, - showRemove = false, - onRemove = {}, + showRemove = supportedMethods.contains(Nip86Method.UNBAN_EVENT), + onRemove = { viewModel.unbanEvent(entry.id) }, ) } } } + + if (supportedMethods.contains(Nip86Method.LIST_ALLOWED_EVENTS)) { + item { Spacer(modifier = Modifier.height(8.dp)) } + item { + SectionHeaderWithAdd( + stringRes(Res.string.relay_management_allowed_events), + showAdd = supportedMethods.contains(Nip86Method.ALLOW_EVENT), + onAdd = { showAllowDialog = true }, + ) + } + + if (allowedEvents.isEmpty()) { + item { EmptyListMessage(stringRes(Res.string.relay_management_no_allowed_events)) } + } else { + items(allowedEvents, key = { "allowed" + it.id }) { entry -> + HexEntryCard( + hex = entry.id, + reason = entry.reason, + showRemove = supportedMethods.contains(Nip86Method.UNALLOW_EVENT), + onRemove = { viewModel.unallowEvent(entry.id) }, + ) + } + } + } + } + + if (showAllowDialog) { + HexInputDialog( + title = stringRes(Res.string.relay_management_allow_event), + label = stringRes(Res.string.relay_management_event_id_hex), + onConfirm = { hex, reason -> + viewModel.allowEvent(hex, reason.ifBlank { null }) + showAllowDialog = false + }, + onDismiss = { showAllowDialog = false }, + ) } if (showBanDialog) { @@ -648,6 +699,7 @@ private fun KindsTab( supportedMethods: List<String>, ) { val allowedKinds by viewModel.allowedKinds.collectAsState() + val disallowedKinds by viewModel.disallowedKinds.collectAsState() var showAddDialog by remember { mutableStateOf(false) } LazyColumn( @@ -665,7 +717,7 @@ private fun KindsTab( if (allowedKinds.isEmpty()) { item { EmptyListMessage(stringRes(Res.string.relay_management_no_allowed_kinds)) } } else { - items(allowedKinds, key = { it }) { kind -> + items(allowedKinds, key = { "allowed$it" }) { kind -> KindEntryCard( kind = kind, showRemove = supportedMethods.contains(Nip86Method.DISALLOW_KIND), @@ -673,6 +725,30 @@ private fun KindsTab( ) } } + + // Read-only: NIP-86 has no "undisallow"; `allowkind` would also turn on the allow list. + if (supportedMethods.contains(Nip86Method.LIST_DISALLOWED_KINDS)) { + item { Spacer(modifier = Modifier.height(8.dp)) } + item { + SectionHeaderWithAdd( + stringRes(Res.string.relay_management_disallowed_kinds), + showAdd = false, + onAdd = {}, + ) + } + + if (disallowedKinds.isEmpty()) { + item { EmptyListMessage(stringRes(Res.string.relay_management_no_disallowed_kinds)) } + } else { + items(disallowedKinds, key = { "disallowed$it" }) { kind -> + KindEntryCard( + kind = kind, + showRemove = false, + onRemove = {}, + ) + } + } + } } if (showAddDialog) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementViewModel.kt index 9b81f386fc..b67f9fa9c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementViewModel.kt @@ -29,11 +29,13 @@ import com.vitorpamplona.amethyst.commons.relayManagement.Nip86Retriever import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip86RelayManagement.Nip86Client +import com.vitorpamplona.quartz.nip86RelayManagement.rpc.AllowedEvent import com.vitorpamplona.quartz.nip86RelayManagement.rpc.AllowedPubkey import com.vitorpamplona.quartz.nip86RelayManagement.rpc.BannedEvent import com.vitorpamplona.quartz.nip86RelayManagement.rpc.BannedPubkey import com.vitorpamplona.quartz.nip86RelayManagement.rpc.BlockedIp import com.vitorpamplona.quartz.nip86RelayManagement.rpc.EventNeedingModeration +import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Method import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Request import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -69,12 +71,18 @@ class RelayManagementViewModel( private val _bannedEvents = MutableStateFlow<List<BannedEvent>>(emptyList()) val bannedEvents: StateFlow<List<BannedEvent>> = _bannedEvents + private val _allowedEvents = MutableStateFlow<List<AllowedEvent>>(emptyList()) + val allowedEvents: StateFlow<List<AllowedEvent>> = _allowedEvents + private val _eventsNeedingModeration = MutableStateFlow<List<EventNeedingModeration>>(emptyList()) val eventsNeedingModeration: StateFlow<List<EventNeedingModeration>> = _eventsNeedingModeration private val _allowedKinds = MutableStateFlow<List<Int>>(emptyList()) val allowedKinds: StateFlow<List<Int>> = _allowedKinds + private val _disallowedKinds = MutableStateFlow<List<Int>>(emptyList()) + val disallowedKinds: StateFlow<List<Int>> = _disallowedKinds + private val _blockedIps = MutableStateFlow<List<BlockedIp>>(emptyList()) val blockedIps: StateFlow<List<BlockedIp>> = _blockedIps @@ -147,6 +155,17 @@ class RelayManagementViewModel( } } + fun loadAllowedEvents() { + viewModelScope.launch { + val response = retriever.execute(client, Nip86Request.listAllowedEvents()) + if (response.error != null) { + _error.value = response.error + } else { + _allowedEvents.value = client.parseAllowedEvents(response)?.distinctBy { it.id } ?: emptyList() + } + } + } + fun loadEventsNeedingModeration() { viewModelScope.launch { val response = retriever.execute(client, Nip86Request.listEventsNeedingModeration()) @@ -169,6 +188,17 @@ class RelayManagementViewModel( } } + fun loadDisallowedKinds() { + viewModelScope.launch { + val response = retriever.execute(client, Nip86Request.listDisallowedKinds()) + if (response.error != null) { + _error.value = response.error + } else { + _disallowedKinds.value = client.parseDisallowedKinds(response)?.distinctBy { it } ?: emptyList() + } + } + } + fun loadBlockedIps() { viewModelScope.launch { val response = retriever.execute(client, Nip86Request.listBlockedIps()) @@ -189,7 +219,9 @@ class RelayManagementViewModel( if (response.error != null) { _error.value = response.error } else { + // NIP-86: banning also drops the pubkey from the allow list. loadBannedPubkeys() + loadIfSupported(Nip86Method.LIST_ALLOWED_PUBKEYS) { loadAllowedPubkeys() } } } } @@ -214,7 +246,9 @@ class RelayManagementViewModel( if (response.error != null) { _error.value = response.error } else { + // NIP-86: allowing also lifts any ban on the pubkey. loadAllowedPubkeys() + loadIfSupported(Nip86Method.LIST_BANNED_PUBKEYS) { loadBannedPubkeys() } } } } @@ -236,6 +270,17 @@ class RelayManagementViewModel( ) { viewModelScope.launch { val response = retriever.execute(client, Nip86Request.banEvent(eventId, reason)) + if (response.error != null) { + _error.value = response.error + } else { + reloadEventLists() + } + } + } + + fun unbanEvent(eventId: String) { + viewModelScope.launch { + val response = retriever.execute(client, Nip86Request.unbanEvent(eventId)) if (response.error != null) { _error.value = response.error } else { @@ -244,6 +289,10 @@ class RelayManagementViewModel( } } + /** + * Approves an event: NIP-86 `allowevent` puts it on the relay's event + * allow list and lifts any ban on it (it no longer means "unban"). + */ fun allowEvent( eventId: String, reason: String? = null, @@ -253,11 +302,35 @@ class RelayManagementViewModel( if (response.error != null) { _error.value = response.error } else { - loadEventsNeedingModeration() + reloadEventLists() } } } + fun unallowEvent(eventId: String) { + viewModelScope.launch { + val response = retriever.execute(client, Nip86Request.unallowEvent(eventId)) + if (response.error != null) { + _error.value = response.error + } else { + loadAllowedEvents() + } + } + } + + private fun reloadEventLists() { + loadIfSupported(Nip86Method.LIST_EVENTS_NEEDING_MODERATION) { loadEventsNeedingModeration() } + loadIfSupported(Nip86Method.LIST_BANNED_EVENTS) { loadBannedEvents() } + loadIfSupported(Nip86Method.LIST_ALLOWED_EVENTS) { loadAllowedEvents() } + } + + private inline fun loadIfSupported( + method: String, + load: () -> Unit, + ) { + if (_supportedMethods.value.contains(method)) load() + } + fun changeRelayName(newName: String) { viewModelScope.launch { val response = retriever.execute(client, Nip86Request.changeRelayName(newName)) @@ -292,6 +365,7 @@ class RelayManagementViewModel( _error.value = response.error } else { loadAllowedKinds() + loadIfSupported(Nip86Method.LIST_DISALLOWED_KINDS) { loadDisallowedKinds() } } } } @@ -303,6 +377,7 @@ class RelayManagementViewModel( _error.value = response.error } else { loadAllowedKinds() + loadIfSupported(Nip86Method.LIST_DISALLOWED_KINDS) { loadDisallowedKinds() } } } } @@ -337,12 +412,13 @@ class RelayManagementViewModel( } fun loadAllLists() { - val methods = _supportedMethods.value - if (methods.contains("listbannedpubkeys")) loadBannedPubkeys() - if (methods.contains("listallowedpubkeys")) loadAllowedPubkeys() - if (methods.contains("listbannedevents")) loadBannedEvents() - if (methods.contains("listeventsneedingmoderation")) loadEventsNeedingModeration() - if (methods.contains("listallowedkinds")) loadAllowedKinds() - if (methods.contains("listblockedips")) loadBlockedIps() + loadIfSupported(Nip86Method.LIST_BANNED_PUBKEYS) { loadBannedPubkeys() } + loadIfSupported(Nip86Method.LIST_ALLOWED_PUBKEYS) { loadAllowedPubkeys() } + loadIfSupported(Nip86Method.LIST_BANNED_EVENTS) { loadBannedEvents() } + loadIfSupported(Nip86Method.LIST_ALLOWED_EVENTS) { loadAllowedEvents() } + loadIfSupported(Nip86Method.LIST_EVENTS_NEEDING_MODERATION) { loadEventsNeedingModeration() } + loadIfSupported(Nip86Method.LIST_ALLOWED_KINDS) { loadAllowedKinds() } + loadIfSupported(Nip86Method.LIST_DISALLOWED_KINDS) { loadDisallowedKinds() } + loadIfSupported(Nip86Method.LIST_BLOCKED_IPS) { loadBlockedIps() } } } diff --git a/cli/README.md b/cli/README.md index 359e2c5787..ddb37079c3 100644 --- a/cli/README.md +++ b/cli/README.md @@ -453,8 +453,12 @@ HTTP endpoint. Reuses quartz's `Nip86Client` and the shared `Nip86Retriever` | `amy admin RELAY supported-methods` | List the NIP-86 methods the relay implements. | | `amy admin RELAY ban-pubkey HEX [--reason R]` / `unban-pubkey HEX` / `list-banned-pubkeys` | Pubkey ban list. | | `amy admin RELAY allow-pubkey HEX [--reason R]` / `unallow-pubkey HEX` / `list-allowed-pubkeys` | Pubkey allow list. | -| `amy admin RELAY ban-event ID [--reason R]` / `allow-event ID` / `list-banned-events` / `list-needing-moderation` | Event moderation. | -| `amy admin RELAY allow-kind N` / `disallow-kind N` / `list-allowed-kinds` | Kind allow list. | +| `amy admin RELAY ban-event ID [--reason R]` / `unban-event ID` / `list-banned-events` | Event ban list. `ban-event` also drops the id from the allow list; `unban-event` does not allow-list it. | +| `amy admin RELAY allow-event ID [--reason R]` / `unallow-event ID` / `list-allowed-events` / `list-needing-moderation` | Event allow list (approve an event: it also lifts any ban) and the moderation queue. | +| `amy admin RELAY create-role ID [--label L] [--description D] [--color HUE] [--order N]` / `edit-role ID …` / `delete-role ID` | NIP-43 member roles (kind 33534); `--color` is a hue 0–360. | +| `amy admin RELAY assign-role HEX ROLE` / `unassign-role HEX ROLE` | Give / take a role. | +| `amy admin RELAY create-claim CODE` / `delete-claim CODE` / `list-claims` | NIP-43 invite codes for kind 28934 join requests. | +| `amy admin RELAY allow-kind N` / `disallow-kind N` / `list-allowed-kinds` / `list-disallowed-kinds` | Kind allow / deny lists. | | `amy admin RELAY block-ip IP [--reason R]` / `unblock-ip IP` / `list-blocked-ips` | IP block list. | | `amy admin RELAY change-name S` / `change-description S` / `change-icon URL` | Relay metadata. | diff --git a/cli/ROADMAP.md b/cli/ROADMAP.md index 09b80c2072..94664b3048 100644 --- a/cli/ROADMAP.md +++ b/cli/ROADMAP.md @@ -116,7 +116,7 @@ vs streaming `subscribe`). Stateless verbs run with no account or network. | `git` | `amy git` | ✅ (events + read) | NIP-34: `init` bootstraps a repo from the local `git` checkout (announce + state, like `ngit init`); repo announce (30617) + state (30618), patches (1617), pull requests (1618/1619), issues (1621), NIP-22 comments (1111), NIP-32 labels (1985), status open/applied/closed/draft (1630-1633), GRASP server list (10317); `issues`/`patches`/`prs`/`thread` reads derive status; `apply` applies a fetched patch to the local tree (`git am`); `browse`/`cat`/`log` read git objects over smart-HTTP v2 (quartz `GitHttpClient`, the same shallow-clone path the Android browser uses). Only git-packfile **push** (writing objects to clone/GRASP servers) and NIP-34 cover notes (1624, no quartz builder yet) are out of scope. Event tag shapes were verified byte-for-byte against the ngit reference implementation and the NIP-34 spec (`clone`/`web` as single multi-value tags, issue `p`-tag for maintainer routing, plain patch/PR `r` tags); the quartz readers stay tolerant of the legacy repeated form. See `quartz/…/nip34Git/GitNip34InteropTest`. | | `podcast` | `amy podcast` | ✅ | NIP-F4 show metadata (10154) + episode publish (54) + list. | | `bunker` | `amy bunker[ connect]` + `amy login bunker://`/`--nostrconnect` | ✅ | NIP-46 remote signer + login, both the `bunker://` and `nostrconnect://` flows, each direction, plus `auth_url` challenge handling (client surfaces the URL + keeps waiting). Interop-verified vs real `nak`. | -| `admin` | `amy admin RELAY METHOD` | ✅ | NIP-86 Relay Management over NIP-98 HTTP auth — full method set (ban/allow pubkey + event, kinds, IP block, change name/desc/icon, list-*). Reuses quartz `Nip86Client` + shared `commons` `Nip86Retriever`. Interop-verified against `amy serve`. | +| `admin` | `amy admin RELAY METHOD` | ✅ | NIP-86 Relay Management over NIP-98 HTTP auth — full method set (ban/allow pubkey + event, NIP-43 roles + invite claims, kinds, IP block, change name/desc/icon, list-*). Reuses quartz `Nip86Client` + shared `commons` `Nip86Retriever`. Interop-verified against `amy serve`. | | `serve` | `amy serve` | ✅ | Embeds **geode** (the standalone Ktor relay on quartz's relay-server code) — in-memory by default, `--db FILE` for SQLite, account is admin so `amy admin` works against it. NIP-86 + NIP-77 included. | | `wallet` (NIP-60 Cashu) | `amy cashu` | ✅ | See the Cashu row above — full NIP-60/61 wallet + nutzaps. | | `mcp` / `fs` / `spell` | — | 🆕 (niche) | MCP server, FUSE mount, MuSig2/FROST; some pull new deps. | diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/AdminCommand.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/AdminCommand.kt index e97f71745c..345f3bbbee 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/AdminCommand.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/AdminCommand.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.cli.DataDir import com.vitorpamplona.amethyst.cli.Output import com.vitorpamplona.amethyst.commons.relayManagement.Nip86Retriever import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip43RelayMembers.roles.RelayRole import com.vitorpamplona.quartz.nip86RelayManagement.Nip86Client import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Request import okhttp3.OkHttpClient @@ -41,10 +42,15 @@ import okhttp3.OkHttpClient * admin wss://relay ban-pubkey HEX [--reason R] / unban-pubkey HEX * admin wss://relay allow-pubkey HEX [--reason R] / list-allowed-pubkeys * admin wss://relay list-banned-pubkeys - * admin wss://relay ban-event ID [--reason R] / allow-event ID / list-banned-events + * admin wss://relay ban-event ID [--reason R] / unban-event ID / list-banned-events + * admin wss://relay allow-event ID [--reason R] / unallow-event ID / list-allowed-events * admin wss://relay list-needing-moderation + * admin wss://relay create-role ID [--label L] [--description D] [--color HUE] [--order N] + * admin wss://relay edit-role ID [...] / delete-role ID + * admin wss://relay assign-role HEX ROLE / unassign-role HEX ROLE + * admin wss://relay create-claim CODE / delete-claim CODE / list-claims * admin wss://relay change-name S / change-description S / change-icon URL - * admin wss://relay allow-kind N / disallow-kind N / list-allowed-kinds + * admin wss://relay allow-kind N / disallow-kind N / list-allowed-kinds / list-disallowed-kinds * admin wss://relay block-ip IP [--reason R] / unblock-ip IP / list-blocked-ips */ object AdminCommand { @@ -58,16 +64,30 @@ object AdminCommand { | admin RELAY allow-pubkey HEX [--reason R] allow-list a pubkey | admin RELAY unallow-pubkey HEX [--reason R] remove a pubkey from the allow-list | admin RELAY list-allowed-pubkeys list allowed pubkeys - | admin RELAY ban-event ID [--reason R] ban an event id - | admin RELAY allow-event ID [--reason R] allow an event id + | admin RELAY ban-event ID [--reason R] ban an event id (drops it from the allow-list) + | admin RELAY unban-event ID [--reason R] lift an event ban (does not allow-list it) | admin RELAY list-banned-events list banned events + | admin RELAY allow-event ID [--reason R] allow-list an event id (lifts any ban) + | admin RELAY unallow-event ID [--reason R] remove an event id from the allow-list + | admin RELAY list-allowed-events list allow-listed events | admin RELAY list-needing-moderation list events flagged for moderation + | admin RELAY create-role ID [--label L] [--description D] [--color HUE] [--order N] + | define a NIP-43 member role (hue 0-360) + | admin RELAY edit-role ID [--label L] [--description D] [--color HUE] [--order N] + | replace a role's definition + | admin RELAY delete-role ID delete a role + | admin RELAY assign-role HEX ROLE give a pubkey a role + | admin RELAY unassign-role HEX ROLE take a role from a pubkey + | admin RELAY list-claims list NIP-43 invite codes the relay accepts + | admin RELAY create-claim CODE create a NIP-43 invite code + | admin RELAY delete-claim CODE revoke a NIP-43 invite code | admin RELAY change-name NAME set the relay's name | admin RELAY change-description TEXT set the relay's description | admin RELAY change-icon URL set the relay's icon | admin RELAY allow-kind N allow an event kind | admin RELAY disallow-kind N disallow an event kind | admin RELAY list-allowed-kinds list allowed kinds + | admin RELAY list-disallowed-kinds list disallowed kinds | admin RELAY block-ip IP [--reason R] block an IP address | admin RELAY unblock-ip IP unblock an IP address | admin RELAY list-blocked-ips list blocked IPs @@ -86,7 +106,12 @@ object AdminCommand { val method = args.positionalOrNull(1) ?: return Output.error("bad_args", "missing method; e.g. supported-methods") val relay = RelayUrlNormalizer.normalizeOrNull(relayArg) ?: return Output.invalidRelayUrl(relayArg) val p2 = args.positionalOrNull(2) + val p3 = args.positionalOrNull(3) val reason = args.flag("reason") + val label = args.flag("label") + val description = args.flag("description") + val color = args.flag("color") + val order = args.flag("order") args.rejectUnknown() fun needArg(name: String): String? = @@ -95,6 +120,19 @@ object AdminCommand { null } + fun needSecondArg(name: String): String? = + p3 ?: run { + Output.error("bad_args", "$method requires a $name argument") + null + } + + val colorInt = + color?.let { + it.toIntOrNull()?.takeIf { hue -> RelayRole.isValidHue(hue) } + ?: return Output.error("bad_args", "--color must be a hue between 0 and 360") + } + val orderInt = order?.let { it.toIntOrNull() ?: return Output.error("bad_args", "--order must be an integer") } + val request: Nip86Request = when (method) { "supported-methods" -> Nip86Request.supportedMethods() @@ -105,15 +143,27 @@ object AdminCommand { "unallow-pubkey" -> Nip86Request.unallowPubkey(needArg("pubkey") ?: return 2, reason) "list-allowed-pubkeys" -> Nip86Request.listAllowedPubkeys() "ban-event" -> Nip86Request.banEvent(needArg("event-id") ?: return 2, reason) - "allow-event" -> Nip86Request.allowEvent(needArg("event-id") ?: return 2, reason) + "unban-event" -> Nip86Request.unbanEvent(needArg("event-id") ?: return 2, reason) "list-banned-events" -> Nip86Request.listBannedEvents() + "allow-event" -> Nip86Request.allowEvent(needArg("event-id") ?: return 2, reason) + "unallow-event" -> Nip86Request.unallowEvent(needArg("event-id") ?: return 2, reason) + "list-allowed-events" -> Nip86Request.listAllowedEvents() "list-needing-moderation" -> Nip86Request.listEventsNeedingModeration() + "create-role" -> Nip86Request.createRole(needArg("role-id") ?: return 2, label, description, colorInt, orderInt) + "edit-role" -> Nip86Request.editRole(needArg("role-id") ?: return 2, label, description, colorInt, orderInt) + "delete-role" -> Nip86Request.deleteRole(needArg("role-id") ?: return 2) + "assign-role" -> Nip86Request.assignRole(needArg("pubkey") ?: return 2, needSecondArg("role-id") ?: return 2) + "unassign-role" -> Nip86Request.unassignRole(needArg("pubkey") ?: return 2, needSecondArg("role-id") ?: return 2) + "list-claims" -> Nip86Request.listClaims() + "create-claim" -> Nip86Request.createClaim(needArg("claim") ?: return 2) + "delete-claim" -> Nip86Request.deleteClaim(needArg("claim") ?: return 2) "change-name" -> Nip86Request.changeRelayName(needArg("name") ?: return 2) "change-description" -> Nip86Request.changeRelayDescription(needArg("description") ?: return 2) "change-icon" -> Nip86Request.changeRelayIcon(needArg("icon-url") ?: return 2) "allow-kind" -> Nip86Request.allowKind((needArg("kind") ?: return 2).toIntOrNull() ?: return Output.error("bad_args", "kind must be an integer")) "disallow-kind" -> Nip86Request.disallowKind((needArg("kind") ?: return 2).toIntOrNull() ?: return Output.error("bad_args", "kind must be an integer")) "list-allowed-kinds" -> Nip86Request.listAllowedKinds() + "list-disallowed-kinds" -> Nip86Request.listDisallowedKinds() "block-ip" -> Nip86Request.blockIp(needArg("ip") ?: return 2, reason) "unblock-ip" -> Nip86Request.unblockIp(needArg("ip") ?: return 2) "list-blocked-ips" -> Nip86Request.listBlockedIps() diff --git a/commonsUI/src/commonMain/composeResources/values/strings.xml b/commonsUI/src/commonMain/composeResources/values/strings.xml index a0b587b986..9e20dddadd 100644 --- a/commonsUI/src/commonMain/composeResources/values/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values/strings.xml @@ -2809,8 +2809,13 @@ <string name="relay_management_no_moderation_events">No events needing moderation</string> <string name="relay_management_banned_events">Banned Events</string> <string name="relay_management_no_banned_events">No banned events</string> + <string name="relay_management_allowed_events">Allowed Events</string> + <string name="relay_management_no_allowed_events">No allowed events</string> + <string name="relay_management_allow_event">Allow Event</string> <string name="relay_management_allowed_kinds">Allowed Kinds</string> <string name="relay_management_no_allowed_kinds">No allowed kinds</string> + <string name="relay_management_disallowed_kinds">Disallowed Kinds</string> + <string name="relay_management_no_disallowed_kinds">No disallowed kinds</string> <string name="relay_management_blocked_ips">Blocked IPs</string> <string name="relay_management_no_blocked_ips">No blocked IPs</string> <string name="relay_management_add">Add</string> @@ -2843,6 +2848,8 @@ <string name="relay_members_join_sent">Join request sent</string> <string name="relay_members_leave_sent">Leave request sent</string> <string name="relay_members_you_are_member">You are a member</string> + <string name="relay_members_invite_code">Invite code</string> + <string name="relay_members_invite_code_hint">This relay admits members with an invite code from its operator</string> <string name="relay_membership_list">Relay membership list</string> <string name="relay_member_added">Member added to relay</string> <string name="relay_members_added">%1$d members added to relay</string> diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip43RelayMembers/ui/RelayMemberCards.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip43RelayMembers/ui/RelayMemberCards.kt index 731198bcec..8ad7a55ca3 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip43RelayMembers/ui/RelayMemberCards.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/nip43RelayMembers/ui/RelayMemberCards.kt @@ -20,18 +20,23 @@ */ package com.vitorpamplona.amethyst.commons.nip43RelayMembers.ui +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -52,6 +57,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.quartz.nip43RelayMembers.addMember.RelayAddMemberEvent import com.vitorpamplona.quartz.nip43RelayMembers.list.RelayMembershipListEvent import com.vitorpamplona.quartz.nip43RelayMembers.removeMember.RelayRemoveMemberEvent +import com.vitorpamplona.quartz.nip43RelayMembers.roles.RelayRole /** NIP-43 kind 13534: the relay's current member list, shown as a count. */ @Composable @@ -123,6 +129,52 @@ fun RelayLeaveRequestCard() { ) } +/** + * The NIP-43 roles (kind 33534) a relay assigned to a member, as small labelled + * chips tinted with each role's hue. Roles without a label show their id; roles + * without a color use the theme's secondary container. Sorted by the roles' + * display `order`. + */ +@Composable +fun RelayRoleChips( + roles: List<RelayRole>, + modifier: Modifier = Modifier, +) { + if (roles.isEmpty()) return + val sorted = remember(roles) { roles.sortedWith(compareBy<RelayRole>({ it.order ?: Int.MAX_VALUE }, { it.label ?: it.id })) } + FlowRow( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + sorted.forEach { RelayRoleChip(it) } + } +} + +@Composable +fun RelayRoleChip(role: RelayRole) { + val hue = role.color + val background = + if (hue != null && RelayRole.isValidHue(hue)) { + Color.hsv(hue.toFloat(), 0.45f, 0.85f) + } else { + MaterialTheme.colorScheme.secondaryContainer + } + val content = if (hue != null && RelayRole.isValidHue(hue)) Color.Black else MaterialTheme.colorScheme.onSecondaryContainer + + Text( + text = role.label ?: role.id, + style = MaterialTheme.typography.labelSmall, + color = content, + maxLines = 1, + modifier = + Modifier + .clip(RoundedCornerShape(50)) + .background(background) + .padding(horizontal = 8.dp, vertical = 2.dp), + ) +} + @Composable private fun RelayMemberEventCard( icon: MaterialSymbol, @@ -175,6 +227,20 @@ private fun RelayMembershipListCardPreview() { } } +@Preview +@Composable +private fun RelayRoleChipsPreview() { + ThemeComparisonColumn { + RelayRoleChips( + listOf( + RelayRole("28b7e50f", label = "king", color = 37, order = 1), + RelayRole("mod", label = "moderator", color = 200, order = 2), + RelayRole("plain"), + ), + ) + } +} + @Preview @Composable private fun RelayAddMemberCardPreview() {